Detecting Back Button/Hash Change in URL - javascript

I just set up my new homepage at http://ritter.vg. I'm using jQuery, but very minimally.
It loads all the pages using AJAX - I have it set up to allow bookmarking by detecting the hash in the URL.
//general functions
function getUrl(u) {
return u + '.html';
}
function loadURL(u) {
$.get(getUrl(u), function(r){
$('#main').html(r);
}
);
}
//allows bookmarking
var hash = new String(document.location).indexOf("#");
if(hash > 0)
{
page = new String(document.location).substring(hash + 1);
if(page.length > 1)
loadURL(page);
else
loadURL('news');
}
else
loadURL('news');
But I can't get the back and forward buttons to work.
Is there a way to detect when the back button has been pressed (or detect when the hash changes) without using a setInterval loop? When I tried those with .2 and 1 second timeouts, it pegged my CPU.

The answers here are all quite old.
In the HTML5 world, you should the use onpopstate event.
window.onpopstate = function(event)
{
alert("location: " + document.location + ", state: " + JSON.stringify(event.state));
};
Or:
window.addEventListener('popstate', function(event)
{
alert("location: " + document.location + ", state: " + JSON.stringify(event.state));
});
The latter snippet allows multiple event handlers to exist, whereas the former will replace any existing handler which may cause hard-to-find bugs.

Use the jQuery hashchange event plugin instead. Regarding your full ajax navigation, try to have SEO friendly ajax. Otherwise your pages shown nothing in browsers with JavaScript limitations.

jQuery BBQ (Back Button & Query Library)
A high quality hash-based browser history plugin and very much up-to-date (Jan 26, 2010) as of this writing (jQuery 1.4.1).

HTML5 has included a much better solution than using hashchange which is the HTML5 State Management APIs - https://developer.mozilla.org/en/DOM/Manipulating_the_browser_history - they allow you to change the url of the page, without needing to use hashes!
Though the HTML5 State Functionality is only available to HTML5 Browsers. So you probably want to use something like History.js which provides a backwards compatible experience to HTML4 Browsers (via hashes, but still supports data and titles as well as the replaceState functionality).
You can read more about it here:
https://github.com/browserstate/History.js

Another great implementation is balupton's jQuery History which will use the native onhashchange event if it is supported by the browser, if not it will use an iframe or interval appropriately for the browser to ensure all the expected functionality is successfully emulated. It also provides a nice interface to bind to certain states.
Another project worth noting as well is jQuery Ajaxy which is pretty much an extension for jQuery History to add ajax to the mix. As when you start using ajax with hashes it get's quite complicated!

I do the following, if you want to use it then paste it in some where and set your handler code in locationHashChanged(qs) where commented, and then call changeHashValue(hashQuery) every time you load an ajax request.
Its not a quick-fix answer and there are none, so you will need to think about it and pass sensible hashQuery args (ie a=1&b=2) to changeHashValue(hashQuery) and then cater for each combination of said args in your locationHashChanged(qs) callback ...
// Add code below ...
function locationHashChanged(qs)
{
var q = parseQs(qs);
// ADD SOME CODE HERE TO LOAD YOUR PAGE ELEMS AS PER q !!
// YOU SHOULD CATER FOR EACH hashQuery ATTRS COMBINATION
// THAT IS PASSED TO changeHashValue(hashQuery)
}
// CALL THIS FROM YOUR AJAX LOAD CODE EACH LOAD ...
function changeHashValue(hashQuery)
{
stopHashListener();
hashValue = hashQuery;
location.hash = hashQuery;
startHashListener();
}
// AND DONT WORRY ABOUT ANYTHING BELOW ...
function checkIfHashChanged()
{
var hashQuery = getHashQuery();
if (hashQuery == hashValue)
return;
hashValue = hashQuery;
locationHashChanged(hashQuery);
}
function parseQs(qs)
{
var q = {};
var pairs = qs.split('&');
for (var idx in pairs) {
var arg = pairs[idx].split('=');
q[arg[0]] = arg[1];
}
return q;
}
function startHashListener()
{
hashListener = setInterval(checkIfHashChanged, 1000);
}
function stopHashListener()
{
if (hashListener != null)
clearInterval(hashListener);
hashListener = null;
}
function getHashQuery()
{
return location.hash.replace(/^#/, '');
}
var hashListener = null;
var hashValue = '';//getHashQuery();
startHashListener();

Try simple & lightweight PathJS lib.
Simple example:
Path.map("#/page").to(function(){
alert('page!');
});

Related

PJax - how do I turn off the modified behaviour

I've got PJax up and running on my test site - it works a treat. However it relies heavily on a lot of javascript widgets and hence leaks memory.
Since I don't have time right now to re-write every widget, I thought that a simple solution would be to do a normal page load after, say 20 pjax page transitions. A simple plan....but it doesn't seem to be possible.
$.pjax.disable();
....still fetches the content via AJAX, but doesn't change the page.
$(document).pjax();.
...doesn't change the behaviour
$.pjax.handleClick = function (event, container, options) { return; };
...doesn't change the behaviour
$.pjax.state.timeout = 0;
...doesn't change the behaviour
delete $.pjax;
...breaks navigation
$.pjax.defaults.timeout=0;
...doesn't change the behaviour
How do I suspend pjax?
If you add a listener for pjax:beforeSend, you can capture the requested URL, set location.href yourself and return false to cancel the pjax behavior. That is how I'm doing it with the following code:
var pageLoadCounter = 0;
var MAX_PAGE_LOADS = 20;
$(".pjaxContainer").on("pjax:beforeSend", function (e, xhr, settings) {
if (++pageLoadCounter > MAX_PAGE_LOADS) {
// URI can be found at https://github.com/medialize/URI.js
var uri = URI(settings.url);
// Remove _pjax from query string before reloading
uri.removeSearch("_pjax");
location.href = uri.toString();
return false;
}
});
I've discovered that changing the id of the pjax container div gives me the desired result - although this seems like a bit of a kludge. It would also be possible by changing the timeout of the ajax request to 0 - but I still need to work out how to do this.
I did ask on the PJax github page about this but so far have not received a response.

How to run a javascript function using the # in the url?

hi this all started when i ran a function (lets call it loadround) that altered the innerHTML of an iframe. now once loadframe was loaded there were links in the iframe that once clicked would change the iframe page. the only problem is when i click the back button the loadround page was gone. i've thought about this numerous times to no avail. so i tried this code.
loadround
then
function loadround(a,b){
window.location.hash = "#loadround('"+a+"','"+b+"')";
var code = "<(h2)>"+a+"</(h2)><(h2)>"+b+"</(h2)>"
var iFrame = document.getElementById('iframe');
var iFrameBody;
iFrameBody = iFrame.contentDocument.getElementsByTagName('body')[0]
iFrameBody.innerHTML = code;
}
(the brackets in the h2 are intentional)
then i would try to reload the function by possibly an onload function but for now i was testing with a simple href as followed.
function check(){
var func = location.hash.replace(/#/, '')
void(func);
}
check
unfortunately the check code doesn't work and im almost certain there is an easier way of doing this. i tried changing the src of the iframe instead of the innerhtml and there was the same problem. thanks in advance
The modern browsers are starting to support the event window.onhashchange
In the meantime you can use the workaround proposed by Lekensteyn or maybe you can find something useful here: JavaScript/jQuery - onhashchange event workaround
You are misunderstanding the function void, which just make sure the return value is undefined. That prevents the browser from navigating away when you put it in a link. You can test that yourself by pasting the next addresses in your browser:
javascript:1 // note: return value 1, browser will print "1" on screen
javascript:void(1) // note: undefined return value, browser won't navigate away
It's strongly discouraged to execute the hash part as Javascript, as it's vulnerable to XSS without proper validating it. You should watch the hash part, and on modification, do something.
An example; watch every 50 milliseconds for modifications in the hash part, and insert in a element with ID targetElement an heading with the hash part. If the hash part is not valid, replace the current entry with home.
var oldHash = '';
function watchHash(){
// strip the first character (#) from location.hash
var newHash = location.hash.substr(1);
if (oldHash != newHash) {
// assume that the parameter are alphanumeric characters or digits
var validated = newHash.match(/^(\w+)$/);
// make sure the hash is valid
if (validated) {
// usually, you would do a HTTP request and use the parameter
var code = "<h1>" + validated[1] + "</h1>";
var element = document.getElementById("targetElement");
element.innerHTML = code;
} else {
// invalid hash, redirect to #home, without creating a new history entry
location.replace("#home");
}
// and set the new state
oldHash = newHash;
}
}
// periodically (every 50 ms) watch for modification in the hash part
setInterval(watchHash, 50);
HTML code:
Home
About Me
Contact
<div id="targetElement">
<!-- HTML will be inserted here -->
</div>

How to get notified about changes of the history via history.pushState?

So now that HTML5 introduces history.pushState to change the browsers history, websites start using this in combination with Ajax instead of changing the fragment identifier of the URL.
Sadly that means that those calls cannot be detect anymore by onhashchange.
My question is: Is there a reliable way (hack? ;)) to detect when a website uses history.pushState? The specification does not state anything about events that are raised (at least I couldn't find anything).
I tried to create a facade and replaced window.history with my own JavaScript object, but it didn't have any effect at all.
Further explanation: I'm developing a Firefox add-on that needs to detect these changes and act accordingly.
I know there was a similar question a few days ago that asked whether listening to some DOM events would be efficient but I would rather not rely on that because these events can be generated for a lot of different reasons.
Update:
Here is a jsfiddle (use Firefox 4 or Chrome 8) that shows that onpopstate is not triggered when pushState is called (or am I doing something wrong? Feel free to improve it!).
Update 2:
Another (side) problem is that window.location is not updated when using pushState (but I read about this already here on SO I think).
5.5.9.1 Event definitions
The popstate event is fired in certain cases when navigating to a session history entry.
According to this, there is no reason for popstate to be fired when you use pushState. But an event such as pushstate would come in handy. Because history is a host object, you should be careful with it, but Firefox seems to be nice in this case. This code works just fine:
(function(history){
var pushState = history.pushState;
history.pushState = function(state) {
if (typeof history.onpushstate == "function") {
history.onpushstate({state: state});
}
// ... whatever else you want to do
// maybe call onhashchange e.handler
return pushState.apply(history, arguments);
};
})(window.history);
Your jsfiddle becomes:
window.onpopstate = history.onpushstate = function(e) { ... }
You can monkey-patch window.history.replaceState in the same way.
Note: of course you can add onpushstate simply to the global object, and you can even make it handle more events via add/removeListener
I do this with simple proxy. This is an alternative to prototype
window.history.pushState = new Proxy(window.history.pushState, {
apply: (target, thisArg, argArray) => {
// trigger here what you need
return target.apply(thisArg, argArray);
},
});
Finally found the "correct" (no monkeypatching, no risk of breaking other code) way to do this! It requires adding a privilege to your extension (which, yes person who helpfully pointed this out in the comments, it's for the extension API which is what was asked for) and using the background page (not just a content script), but it does work.
The event you want is browser.webNavigation.onHistoryStateUpdated, which is fired when a page uses the history API to change the URL. It only fires for sites that you have permission to access, and you can also use a URL filter to further cut down on the spam if you need to. It requires the webNavigation permission (and of course host permission for the relevant domain(s)).
The event callback gets the tab ID, the URL that is being "navigated" to, and other such details. If you need to take an action in the content script on that page when the event fires, either inject the relevant script directly from the background page, or have the content script open a port to the background page when it loads, have the background page save that port in a collection indexed by tab ID, and send a message across the relevant port (from the background script to the content script) when the event fires.
Thank #KalanjDjordjeDjordje for his answer. I tried to make his idea a complete solution:
const onChangeState = (state, title, url, isReplace) => {
// define your listener here ...
}
// set onChangeState() listener:
['pushState', 'replaceState'].forEach((changeState) => {
// store original values under underscored keys (`window.history._pushState()` and `window.history._replaceState()`):
window.history['_' + changeState] = window.history[changeState]
window.history[changeState] = new Proxy(window.history[changeState], {
apply (target, thisArg, argList) {
const [state, title, url] = argList
onChangeState(state, title, url, changeState === 'replaceState')
return target.apply(thisArg, argList)
},
})
})
I used to use this:
var _wr = function(type) {
var orig = history[type];
return function() {
var rv = orig.apply(this, arguments);
var e = new Event(type);
e.arguments = arguments;
window.dispatchEvent(e);
return rv;
};
};
history.pushState = _wr('pushState'), history.replaceState = _wr('replaceState');
window.addEventListener('replaceState', function(e) {
console.warn('THEY DID IT AGAIN!');
});
It's almost the same as galambalazs did.
It's usually overkill though. And it might not work in all browsers. (I only care about my version of my browser.)
(And it leaves a var _wr, so you might want to wrap it or something. I didn't care about that.)
In addition to other answers. Instead of storing the original function, we can use the History interface.
history.pushState = function()
{
// ...
History.prototype.pushState.apply(history, arguments);
}
I'd rather not overwrite the native history method so this simple implementation creates my own function called eventedPush state which just dispatches an event and returns history.pushState(). Either way works fine but I find this implementation a bit cleaner as native methods will continue to perform as future developers expect.
function eventedPushState(state, title, url) {
var pushChangeEvent = new CustomEvent("onpushstate", {
detail: {
state,
title,
url
}
});
document.dispatchEvent(pushChangeEvent);
return history.pushState(state, title, url);
}
document.addEventListener(
"onpushstate",
function(event) {
console.log(event.detail);
},
false
);
eventedPushState({}, "", "new-slug");
galambalazs's answer monkey patches window.history.pushState and window.history.replaceState, but for some reason it stopped working for me. Here's an alternative that's not as nice because it uses polling:
(function() {
var previousState = window.history.state;
setInterval(function() {
if (previousState !== window.history.state) {
previousState = window.history.state;
myCallback();
}
}, 100);
})();
Since you're asking about a Firefox addon, here's the code that I got to work. Using unsafeWindow is no longer recommended, and errors out when pushState is called from a client script after being modified:
Permission denied to access property history.pushState
Instead, there's an API called exportFunction which allows the function to be injected into window.history like this:
var pushState = history.pushState;
function pushStateHack (state) {
if (typeof history.onpushstate == "function") {
history.onpushstate({state: state});
}
return pushState.apply(history, arguments);
}
history.onpushstate = function(state) {
// callback here
}
exportFunction(pushStateHack, unsafeWindow.history, {defineAs: 'pushState', allowCallbacks: true});
Well, I see many examples of replacing the pushState property of history but I'm not sure that's a good idea, I'd prefer to create a service event based with a similar API to history that way you can control not only push state but replace state as well and it open doors for many other implementations not relying on global history API. Please check the following example:
function HistoryAPI(history) {
EventEmitter.call(this);
this.history = history;
}
HistoryAPI.prototype = utils.inherits(EventEmitter.prototype);
const prototype = {
pushState: function(state, title, pathname){
this.emit('pushstate', state, title, pathname);
this.history.pushState(state, title, pathname);
},
replaceState: function(state, title, pathname){
this.emit('replacestate', state, title, pathname);
this.history.replaceState(state, title, pathname);
}
};
Object.keys(prototype).forEach(key => {
HistoryAPI.prototype = prototype[key];
});
If you need the EventEmitter definition, the code above is based on the NodeJS event emitter: https://github.com/nodejs/node/blob/36732084db9d0ff59b6ce31e839450cd91a156be/lib/events.js. utils.inherits implementation can be found here: https://github.com/nodejs/node/blob/36732084db9d0ff59b6ce31e839450cd91a156be/lib/util.js#L970
Based on the solution given by #gblazex, in case you want to follow the same approach, but using arrow functions, follow up the below example in your javascript logic:
private _currentPath:string;
((history) => {
//tracks "forward" navigation event
var pushState = history.pushState;
history.pushState =(state, key, path) => {
this._notifyNewUrl(path);
return pushState.apply(history,[state,key,path]);
};
})(window.history);
//tracks "back" navigation event
window.addEventListener('popstate', (e)=> {
this._onUrlChange();
});
Then, implement another function _notifyUrl(url) that triggers any required action you may need when the current page url is updated ( even if the page has not been loaded at all )
private _notifyNewUrl (key:string = window.location.pathname): void {
this._path=key;
// trigger whatever you need to do on url changes
console.debug(`current query: ${this._path}`);
}
Since I just wanted the new URL, I've adapted the codes of #gblazex and #Alberto S. to get this:
(function(history){
var pushState = history.pushState;
history.pushState = function(state, key, path) {
if (typeof history.onpushstate == "function") {
history.onpushstate({state: state, path: path})
}
pushState.apply(history, arguments)
}
window.onpopstate = history.onpushstate = function(e) {
console.log(e.path)
}
})(window.history);
I don't think it's an good idea do modify native functions even if you can, and you should always keep your application scope, so an good approach is not using the global pushState function, instead, use one of your own:
function historyEventHandler(state){
// your stuff here
}
window.onpopstate = history.onpushstate = historyEventHandler
function pushHistory(...args){
history.pushState(...args)
historyEventHandler(...args)
}
<button onclick="pushHistory(...)">Go to happy place</button>
Notice that if any other code use the native pushState function, you will not get an event trigger (but if this happens, you should check your code)
You could bind to the window.onpopstate event?
https://developer.mozilla.org/en/DOM%3awindow.onpopstate
From the docs:
An event handler for the popstate
event on the window.
A popstate event is dispatched to the
window every time the active history
entry changes. If the history entry
being activated was created by a call
to history.pushState() or was affected
by a call to history.replaceState(),
the popstate event's state property
contains a copy of the history entry's
state object.
I think this topic needs a more modern solution.
I'm sure nsIWebProgressListener was around back then I'm surprised no one mentioned it.
From a framescript (for e10s compatability):
let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebProgress);
webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_WINDOW | Ci.nsIWebProgress.NOTIFY_LOCATION);
Then listening in the onLoacationChange
onLocationChange: function onLocationChange(webProgress, request, locationURI, flags) {
if (flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT
That will apparently catch all pushState's. But there is a comment warning that it "ALSO triggers for pushState". So we need to do some more filtering here to ensure it's just pushstate stuff.
Based on: https://github.com/jgraham/gecko/blob/55d8d9aa7311386ee2dabfccb481684c8920a527/toolkit/modules/addons/WebNavigation.jsm#L18
And: resource://gre/modules/WebNavigationContent.js
As standard states:
Note that just calling history.pushState() or history.replaceState() won't trigger a popstate event. The popstate event is only triggered by doing a browser action such as clicking on the back button (or calling history.back() in JavaScript)
we need to call history.back() to trigeer WindowEventHandlers.onpopstate
So insted of:
history.pushState(...)
do:
history.pushState(...)
history.pushState(...)
history.back()

using javascript to track another javascript script?

I was just wondering whether there are any way (libraries, frameworks, tutorials) to do javascript tracking with another script? Basically, i want to track as the user work with the site, which function gets executed with what parameters and so on, as detailed as possible.
thanks a lot!
The extent of detail you're expecting will be challenging for any solution to gather and report on without severely slowing down your scripts -- consider that, for every call, at least 1 other call would need to occur to gather this.
You'd be better to pick a few key events (mouse clicks, etc.) and track only a few details (such as time) for them. If you're using ajax, keep JavaScript and the browser oblivious and just track this on server-side.
There's a few options but I'm not sure if there are any "great" ones. I take it Firebug/IE Dev toolbar profiling won't work because you are trying to track remote user's actions.
So, one option (I'm not highly recommending for production purposes), will work in some but not all browsers.
Essentially you overwrite every function, with a wrapper that you then inject your logging.
(I haven't tested this, trying to recall it from memory... hopefully in "pseudo code" you get the idea...)
//e.g. get all functions defined on the global window object
function logAll(){
var funcs = [];
var oldFunc;
for(var i in window){
try {
if(typeof(window[i]) == 'function'){
if(i != 'logAll'){
funcs.push(i);
}
}
} catch(ex){
//handle as desired
}
}
var x;
for(var i in funcs){
x = '_' + new Date().getTime();
window[x] = window[i];//save the old function as new function
//redefine original
window[i] = function(){
//do your logging here...
//then call the real function (and pass all params along)
call(window[x]);
};
}
};

Javascript: how do I determine if a link targets the same domain as the page it resides on?

For the purposes of tracking non-HTML documents via google analytics, I need the mentioned algorithm. It should:
not hard-code the domain
ignore the protocol (i.e. http/https)
not worry about the presence/absence of "www" (any absolute links WILL prefix with "www" and all pages WILL be served via "www")
This is complicated by the fact that I need to access it via a function called from the IE-only 'attachEvent'.
UPDATE Sorry, I've worded this question really badly. The real problem is getting this to work via an event, since IE has its own made-up world of event handling. Take the following:
function add_event(obj) {
if (obj.addEventListener)
obj.addEventListener('click', track_file, true);
else if (obj.attachEvent)
obj.attachEvent("on" + 'click', track_file);
}
function track_file(obj) { }
It seems as if the "obj" in track_file is not the same across browsers - how can I refer to what was clicked in IE?
I would like to point out that, if you're on so.com, the following links are URLs within the same domain:
http://test.so.com
http://so.com/index
index
/index
#
/#
https://subdomain.so.com#hash
mail.google.com
mail.google.com/index.php?var=value#index
(it may seem odd, but the last two ones are valid: if you're on http://so.com, the last one would take you to http://so.com/mail.google.com/index.php?var=value, which is perfectly valid)
This doesn't really answer the question but I hope it will guide the rest of the answers. If there's anything else weird enough, feel free to add it.
This sounds like a comedy answer but in all seriousness it would be be advisable that you could also do something like:
$('a.external')
Certainly the regex comparison to your window.location is the programmatic answer.
The method of attachment is not the only way IE and W3 event listeners differ. For IE you must read window.event.srcElement; in W3 it's event.target where event is the parameter passed to the callback function.
If you don't need multiple event handlers on links, old-school DOM 0 event handlers are probably an easier way for you to approach this, allowing you to just us ‘this’ to get the object on any browser.
function bindtolinks() {
for (var i= document.links.length; i-->0;)
document.links.onclick= clicklink;
}
function clicklink() {
if (this.host==window.location.host) {
dosomething();
return true; // I'm an internal link. Follow me.
} else {
dosomethingelse();
return false; // I'm an external link. Don't follow, only do something else.
}
}
I will answer the question in the update, about events in IE:
function track_file(evt)
{
if (evt == undefined)
{
evt = window.event; // For IE
}
// Use evt
}
is the classical way to get consistent event object across browsers.
After that, I would use regexes to normalize the URL, but I am not sure what you look after.
[EDIT] Some real code to put in practice what I wrote above... :-)
function CheckTarget(evt)
{
if (evt == undefined)
{
// For IE
evt = window.event;
//~ event.returnValue = false;
var target = evt.srcElement;
var console = { log: alert };
}
else
{
target = evt.target;
//~ preventDefault();
}
alert(target.hostname + " vs. " + window.location.hostname);
var re = /^https?:\/\/[\w.-]*?([\w-]+\.[a-z]+)\/.*$/;
var strippedURL = window.location.href.match(re);
if (strippedURL == null)
{
// Oops! (?)
alert("Where are we?");
return false;
}
alert(window.location.href + " => " + strippedURL);
var strippedTarget = target.href.match(re);
if (strippedTarget == null)
{
// Oops! (?)
alert("What is it?");
return false;
}
alert(target + " => " + strippedTarget);
if (strippedURL[1] == strippedTarget[1])
{
//~ window.location.href = target.href; // Go there
return true; // Accept the jump
}
return false;
}
That's test code, not production code, obviously!
The lines with //~ comments show the alternative way of preventing the click on link to do the jump. It is, somehow, more efficient because if I use Firebug's console.log, curiously the return false is ineffective.
I used here the behavior "follow link or not", not knowing the real final purpose.
As pointed out in comments, the RE can be simpler by using hostname instead of href... I leave as it because it was already coded and might be useful in other cases.
Some special precautions should be taken in both cases to handle special cases, like localhost, IP addresses, ports...
I got rid of the domain name, before re-reading the question and seeing it wasn't a problem... Well, perhaps it can be useful to somebody else.
Note: I shown a similar solution in a question to decorate links: Editing all external links with javascript
Given a click event and the original target element, this should work for the original question:
if(target.protocol == window.location.protocol && target.host == window.location.host){
}
Browsers nicely convert the link from the various patterns mentioned by #Tom into full links, so the protocol and host values simply need to match your domain.
if( someDomElementWhichIsALink.href.indexOf(window.location) != -1 ) {
// this is targeting your domain
}

Categories