193

I have a sliding panel set up on my website.

When it finished animating, I set the hash like so

function() {
   window.location.hash = id;
}

(this is a callback, and the id is assigned earlier).

This works good, to allow the user to bookmark the panel, and also for the non JavaScript version to work.

However, when I update the hash, the browser jumps to the location. I guess this is expected behaviour.

My question is: how can I prevent this? I.e. how can I change the window's hash, but not have the browser scroll to the element if the hash exists? Some sort of event.preventDefault() sort of thing?

I'm using jQuery 1.4 and the scrollTo plugin.

Many thanks!

Update

Here is the code that changes the panel.

$('#something a').click(function(event) {
    event.preventDefault();
    var link = $(this);
    var id = link[0].hash;

    $('#slider').scrollTo(id, 800, {
        onAfter: function() {

            link.parents('li').siblings().removeClass('active');
            link.parent().addClass('active');
            window.location.hash = id;

            }
    });
});
alex
  • 479,566
  • 201
  • 878
  • 984

9 Answers9

307

There is a workaround by using the history API on modern browsers with fallback on old ones:

if(history.pushState) {
    history.pushState(null, null, '#myhash');
}
else {
    location.hash = '#myhash';
}

Credit goes to Lea Verou

Attila Fulop
  • 6,861
  • 2
  • 44
  • 50
  • 11
    Note that pushState has the side- (indented) effect of adding a state to the browser history stack. In other words, when the user clicks the back button, nothing will happen unless you also add a popstate event listener. – David Cook Sep 02 '14 at 02:42
  • 42
    @DavidCook - We could also use `history.replaceState` (which, for hash changes, might make more sense) to avoid the need for a `popstate` event listener. – Jack Sep 06 '14 at 00:38
  • This worked for me. I like the effect of the history change. I want to add the caveat that this will not trigger the `hashchange` event. That was something I had to work around. – Jordan Jul 02 '15 at 19:17
  • 1
    warning! this will not trigger a `hashchange` event – santiago arizti Oct 22 '18 at 22:46
  • 1
    You **don't need** to use [`history.pushState`](https://devdocs.io/dom/history_api#The_pushState()_method) to workaround this behavior. If your [`:target`](https://devdocs.io/css/:target) element has [`position: fixed`](https://devdocs.io/css/position#fixed) it is removed from the normal flow so there's nowhere to scroll to. You may then assign the hash using `window.history.hash = 'foo'` and the window will stay put. – vhs Feb 01 '19 at 08:00
  • The second parameter to pushState should be a string (empty or not). `null` and `undefined` are not valid per the specification and typescript typings. BTW this is the `title` value and currently ignored by some implementations. `pushState(null, '', '#myhash')` or `pushState(null, 'My Page', '#myhash')` – Steven Spungin Jul 20 '19 at 14:05
  • This doesn't seem to work anymore. If you have an element with id="myhash", replaceState and pushState will anchor to that element. – Tom Roggero May 19 '20 at 12:57
56

The problem is you are setting the window.location.hash to an element's ID attribute. It is the expected behavior for the browser to jump to that element, regardless of whether you "preventDefault()" or not.

One way to get around this is to prefix the hash with an arbitrary value like so:

window.location.hash = 'panel-' + id.replace('#', '');

Then, all you need to do is to check for the prefixed hash on page load. As an added bonus, you can even smooth scroll to it since you are now in control of the hash value...

$(function(){
    var h = window.location.hash.replace('panel-', '');
    if (h) {
        $('#slider').scrollTo(h, 800);
    }
});

If you need this to work at all times (and not just on the initial page load), you can use a function to monitor changes to the hash value and jump to the correct element on-the-fly:

var foundHash;
setInterval(function() {
    var h = window.location.hash.replace('panel-', '');
    if (h && h !== foundHash) {
        $('#slider').scrollTo(h, 800);
        foundHash = h;
    }
}, 100);
Derek Hunziker
  • 12,996
  • 4
  • 57
  • 105
  • 3
    This is the only solution that actually answers the question - allowing hashing without jumping – amosmos Feb 03 '15 at 22:31
  • This really answers the question explaining the addition and deletion of the arbitrary value for the hash to avoid the jump as per default browser behaviour. – lowtechsun May 27 '16 at 01:02
  • 3
    good solution, but weakens the OPs solution as it negates the use of bookmarking sections – MikeyBeLike Jun 03 '16 at 21:16
32

Cheap and nasty solution.. Use the ugly #! style.

To set it:

window.location.hash = '#!' + id;

To read it:

id = window.location.hash.replace(/^#!/, '');

Since it doesn't match and anchor or id in the page, it won't jump.

Gavin Brock
  • 5,027
  • 1
  • 30
  • 33
  • 4
    I had to preserve compatibility with IE 7 and some mobile versions of the site. This solution was cleanest for me. Generally I'd be all over the pushState solutions. – Eric Goodwin May 02 '14 at 17:14
  • 2
    setting the hash with: ```window.location.hash = '#/' + id;``` and then replacing it with: ```window.location.hash.replace(/^#\//, '#');``` will prettify the url a bit -> ```projects/#/tab1``` – braitsch Nov 07 '15 at 00:50
12

Why dont you get the current scroll position, put it in a variable then assign the hash and put the page scroll back to where it was:

var yScroll=document.body.scrollTop;
window.location.hash = id;
document.body.scrollTop=yScroll;

this should work

user706270
  • 131
  • 1
  • 3
  • 2
    hm, this was working for me for awhile, but sometime in the last few months this has stopped working on firefox.... – matchew May 28 '13 at 21:26
11

I used a combination of Attila Fulop (Lea Verou) solution for modern browsers and Gavin Brock solution for old browsers as follows:

if (history.pushState) {
    // IE10, Firefox, Chrome, etc.
    window.history.pushState(null, null, '#' + id);
} else {
    // IE9, IE8, etc
    window.location.hash = '#!' + id;
}

As observed by Gavin Brock, to capture the id back you will have to treat the string (which in this case can have or not the "!") as follows:

id = window.location.hash.replace(/^#!?/, '');

Before that, I tried a solution similar to the one proposed by user706270, but it did not work well with Internet Explorer: as its Javascript engine is not very fast, you can notice the scroll increase and decrease, which produces a nasty visual effect.

aldemarcalazans
  • 1,309
  • 13
  • 16
4

This solution worked for me.

The problem with setting location.hash is that the page will jump to that id if it's found on the page.

The problem with window.history.pushState is that it adds an entry to the history for each tab the user clicks. Then when the user clicks the back button, they go to the previous tab. (this may or may not be what you want. it was not what I wanted).

For me, replaceState was the better option in that it only replaces the current history, so when the user clicks the back button, they go to the previous page.

$('#tab-selector').tabs({
  activate: function(e, ui) {
    window.history.replaceState(null, null, ui.newPanel.selector);
  }
});

Check out the History API docs on MDN.

Mike Vallano
  • 326
  • 4
  • 4
3

This solution worked for me

// store the currently selected tab in the hash value
    if(history.pushState) {
        window.history.pushState(null, null, '#' + id);
    }
    else {
        window.location.hash = id;
    }

// on load of the page: switch to the currently selected tab
var hash = window.location.hash;
$('#myTab a[href="' + hash + '"]').tab('show');

And my full js code is

$('#myTab a').click(function(e) {
  e.preventDefault();
  $(this).tab('show');
});

// store the currently selected tab in the hash value
$("ul.nav-tabs > li > a").on("shown.bs.tab", function(e) {
  var id = $(e.target).attr("href").substr(1);
    if(history.pushState) {
        window.history.pushState(null, null, '#' + id);
    }
    else {
        window.location.hash = id;
    }
   // window.location.hash = '#!' + id;
});

// on load of the page: switch to the currently selected tab
var hash = window.location.hash;
// console.log(hash);
$('#myTab a[href="' + hash + '"]').tab('show');
Rohit Dhiman
  • 2,691
  • 18
  • 33
1

I'm not sure if you can alter the original element but how about switch from using the id attr to something else like data-id? Then just read the value of data-id for your hash value and it won't jump.

Jon B
  • 2,444
  • 2
  • 18
  • 19
0

When using laravel framework, I had some issues with using a route->back() function since it erased my hash. In order to keep my hash, I created a simple function:

$(function() {   
    if (localStorage.getItem("hash")    ){
     location.hash = localStorage.getItem("hash");
    }
}); 

and I set it in my other JS function like this:

localStorage.setItem("hash", myvalue);

You can name your local storage values any way you like; mine named hash.

Therefore, if the hash is set on PAGE1 and then you navigate to PAGE2; the hash will be recreated on PAGE1 when you click Back on PAGE2.

Andrew
  • 7,619
  • 13
  • 63
  • 117