Modifying location.hash without page scrolling
Asked Answered
S

20

171

We've got a few pages using ajax to load in content and there's a few occasions where we need to deep link into a page. Instead of having a link to "Users" and telling people to click "settings" it's helpful to be able to link people to user.aspx#settings

To allow people to provide us with correct links to sections (for tech support, etc.) I've got it set up to automatically modify the hash in the URL whenever a button is clicked. The only issue of course is that when this happens, it also scrolls the page to this element.

Is there a way to disable this? Below is how I'm doing this so far.

$(function(){
    //This emulates a click on the correct button on page load
    if(document.location.hash){
     $("#buttons li a").removeClass('selected');
     s=$(document.location.hash).addClass('selected').attr("href").replace("javascript:","");
     eval(s);
    }

    //Click a button to change the hash
    $("#buttons li a").click(function(){
            $("#buttons li a").removeClass('selected');
            $(this).addClass('selected');
            document.location.hash=$(this).attr("id")
            //return false;
    });
});

I had hoped the return false; would stop the page from scrolling - but it just makes the link not work at all. So that's just commented out for now so I can navigate.

Any ideas?

Shirk answered 28/9, 2009 at 22:8 Comment(0)
S
94

I think I may have found a fairly simple solution. The problem is that the hash in the URL is also an element on the page that you get scrolled to. if I just prepend some text to the hash, now it no longer references an existing element!

$(function(){
    //This emulates a click on the correct button on page load
    if(document.location.hash){
     $("#buttons li a").removeClass('selected');
     s=$(document.location.hash.replace("btn_","")).addClass('selected').attr("href").replace("javascript:","");
     eval(s);
    }

    //Click a button to change the hash
    $("#buttons li a").click(function(){
            $("#buttons li a").removeClass('selected');
            $(this).addClass('selected');
            document.location.hash="btn_"+$(this).attr("id")
            //return false;
    });
});

Now the URL appears as page.aspx#btn_elementID which is not a real ID on the page. I just remove "btn_" and get the actual element ID

Shirk answered 2/10, 2009 at 20:1 Comment(4)
Great solution. Most painless of the lot.Iterative
Note that if you're already committed to page.aspx#elementID URLs for some reason you can reverse this technique and prepend "btn_" to all of your IDsKentiggerma
Does not work if you are using :target pseudo-selectors in CSS.Scraper
Love this solution! We're using the URL hash for AJAX based navigation, prepending the IDs with a / seems logical.Disulfide
E
122

Use history.replaceState or history.pushState* to change the hash. This will not trigger the jump to the associated element.

Example

$(document).on('click', 'a[href^=#]', function(event) {
  event.preventDefault();
  history.pushState({}, '', this.href);
});

Demo on JSFiddle

* If you want history forward and backward support

History behaviour

If you are using history.pushState and you don't want page scrolling when the user uses the history buttons of the browser (forward/backward) check out the experimental scrollRestoration setting (Chrome 46+ only).

history.scrollRestoration = 'manual';

Browser Support

Engineering answered 28/1, 2013 at 11:9 Comment(8)
replaceState is probably the better way to go here. The difference being pushState adds an item to your history while replaceState does not.Dakota
It's not always the body that scrolls, sometimes it's the documentElement. See this gist by Diego PeriniAmata
any idea on ie8/9 here?Referendum
pushState is the way to go if you want the back / forward buttons to work in the way you users may expect.Workman
@Referendum IE 8/9 do not support pushState. caniuse.com/#search=pushStateVenosity
@Referendum check out: stackoverflow.com/revisions/…Engineering
This doesn't seem to work work when I have the element as a jquery element in JS and call $element.trigger("click"). The event fires, but the scroll jump still happens. Also your answer doesn't have any quotes around the #, so you will get an unrecognized expression error on a[href^=]Buffet
Changing the hash/fragment via the History API doesn't appear to update the :target CSS selector for me. The same happens with the "ID-defusing" suggested in other answers.Featherveined
K
111

Step 1: You need to defuse the node ID, until the hash has been set. This is done by removing the ID off the node while the hash is being set, and then adding it back on.

hash = hash.replace( /^#/, '' );
var node = $( '#' + hash );
if ( node.length ) {
  node.attr( 'id', '' );
}
document.location.hash = hash;
if ( node.length ) {
  node.attr( 'id', hash );
}

Step 2: Some browsers will trigger the scroll based on where the ID'd node was last seen so you need to help them a little. You need to add an extra div to the top of the viewport, set its ID to the hash, and then roll everything back:

hash = hash.replace( /^#/, '' );
var fx, node = $( '#' + hash );
if ( node.length ) {
  node.attr( 'id', '' );
  fx = $( '<div></div>' )
          .css({
              position:'absolute',
              visibility:'hidden',
              top: $(document).scrollTop() + 'px'
          })
          .attr( 'id', hash )
          .appendTo( document.body );
}
document.location.hash = hash;
if ( node.length ) {
  fx.remove();
  node.attr( 'id', hash );
}

Step 3: Wrap it in a plugin and use that instead of writing to location.hash...

Kiddy answered 28/9, 2009 at 23:6 Comment(11)
For the browsers targeted by step 2, will that step cause the page to jump to the top if its not already there?Luht
No, what is happening in step 2 is that a hidden div is created and placed at the current location of the scroll. It's the same visually as position:fixed/top:0. Thus the scrollbar is "moved" to the exact same spot it currently is on.Kiddy
This solution indeed works well - however the line: top: $.scroll().top + 'px' Should be: top: $(window).scrollTop() + 'px'Dodie
It would be useful to know which browsers need step 2 here.Spectator
Thanks Borgar. It puzzles me though that you're appending the fx div before deleting the target node's id. That means there's an instant in which there are duplicate ID's in the document. Seems like a potential issue, or at least bad manners ;)Redblooded
I've just updated the answer fixing the problems pointed out in the comments here.Kiddy
Thank you so much, this also helped me. But I also noticed a side effect when this is in action: I often scroll by locking the "middle mouse button" and simply moving the mouse into the direction I want to scroll. In Google Chrome this interrupts the scroll flow. Is there maybe a fancy workaround for that?Unable
Step 2 is causing some jumpiness in one place for me (I'm assuming based on CSS that gets applied to the placeholder element) and I'm wondering if I can remove it - as @Spectator notes, it would be good to know which browsers "some browsers" refers to. It appears that removing the added fx element removes the jumpiness in IE8 and Chrome at the very least while retaining the effectiveness of the hash change. I know this answer was posted four years ago now – is "some browsers" IE6? IE7? …Opera? Is it safe to remove fx if not supporting the likes of IE6/7?Feed
The scroll jump behavior, as I recall, was not only limited to some browsers but also to the distance to the target ID. If you want to know the details, the "Ask Question" button is your friend. Also testing it. If the issue does not affect any of the browsers you support then don't waste your time with it. --- Secondly, to debug CSS getting applied to the placeholder, use a debug tool like Firebug or Chrome Developer tools. All you have to do is run the placeholder injection code and then inspect the element.Kiddy
Removing the id is messing up with all the event handlersStupidity
@Chris-Barr @Kiddy Got here by reference from the faq module faq.js While the hash is changing in the url when clicking on different faq nodes, I would like to be able to enable the scroll to page of that faq when accessing it via that url in browser with hash tag, if that makes sense? Wonder if that's even possible as the faq nodes after js processing contain: <div class="faq-question-answer"> <div class="faq-question faq-dt-hide-answer faq-processed"> <span datatype="" property="dc:title"><a href="/my-test-faq" id="t50n173">FAQ question title?</a></span>Caelum
S
94

I think I may have found a fairly simple solution. The problem is that the hash in the URL is also an element on the page that you get scrolled to. if I just prepend some text to the hash, now it no longer references an existing element!

$(function(){
    //This emulates a click on the correct button on page load
    if(document.location.hash){
     $("#buttons li a").removeClass('selected');
     s=$(document.location.hash.replace("btn_","")).addClass('selected').attr("href").replace("javascript:","");
     eval(s);
    }

    //Click a button to change the hash
    $("#buttons li a").click(function(){
            $("#buttons li a").removeClass('selected');
            $(this).addClass('selected');
            document.location.hash="btn_"+$(this).attr("id")
            //return false;
    });
});

Now the URL appears as page.aspx#btn_elementID which is not a real ID on the page. I just remove "btn_" and get the actual element ID

Shirk answered 2/10, 2009 at 20:1 Comment(4)
Great solution. Most painless of the lot.Iterative
Note that if you're already committed to page.aspx#elementID URLs for some reason you can reverse this technique and prepend "btn_" to all of your IDsKentiggerma
Does not work if you are using :target pseudo-selectors in CSS.Scraper
Love this solution! We're using the URL hash for AJAX based navigation, prepending the IDs with a / seems logical.Disulfide
W
4

I was recently building a carousel which relies on window.location.hash to maintain state and made the discovery that Chrome and webkit browsers will force scrolling (even to a non visible target) with an awkward jerk when the window.onhashchange event is fired.

Even attempting to register a handler which stops propogation:

$(window).on("hashchange", function(e) { 
  e.stopPropogation(); 
  e.preventDefault(); 
});

Did nothing to stop the default browser behavior. The solution I found was using window.history.pushState to change the hash without triggering the undesirable side-effects.

 $("#buttons li a").click(function(){
    var $self, id, oldUrl;

    $self = $(this);
    id = $self.attr('id');

    $self.siblings().removeClass('selected'); // Don't re-query the DOM!
    $self.addClass('selected');

    if (window.history.pushState) {
      oldUrl = window.location.toString(); 
      // Update the address bar 
      window.history.pushState({}, '', '#' + id);
      // Trigger a custom event which mimics hashchange
      $(window).trigger('my.hashchange', [window.location.toString(), oldUrl]);
    } else {
      // Fallback for the poors browsers which do not have pushState
      window.location.hash = id;
    }

    // prevents the default action of clicking on a link.
    return false;
});

You can then listen for both the normal hashchange event and my.hashchange:

$(window).on('hashchange my.hashchange', function(e, newUrl, oldUrl){
  // @todo - do something awesome!
});
Workman answered 25/3, 2015 at 13:2 Comment(1)
of course my.hashchange is just an example event nameWorkman
D
2

A snippet of your original code:

$("#buttons li a").click(function(){
    $("#buttons li a").removeClass('selected');
    $(this).addClass('selected');
    document.location.hash=$(this).attr("id")
});

Change this to:

$("#buttons li a").click(function(e){
    // need to pass in "e", which is the actual click event
    e.preventDefault();
    // the preventDefault() function ... prevents the default action.
    $("#buttons li a").removeClass('selected');
    $(this).addClass('selected');
    document.location.hash=$(this).attr("id")
});
Delacroix answered 22/7, 2010 at 18:37 Comment(1)
This won't work because setting the hash property will cause the page to scroll anyway.Hollandia
B
2

Okay, this is a rather old topic but I thought I'd chip in as the 'correct' answer doesn't work well with CSS.

This solution basically prevents the click event from moving the page so we can get the scroll position first. Then we manually add the hash and the browser automatically triggers a hashchange event. We capture the hashchange event and scroll back to the correct position. A callback separates and prevents your code causing a delay by keeping your hash hacking in one place.

var hashThis = function( $elem, callback ){
    var scrollLocation;
    $( $elem ).on( "click", function( event ){
        event.preventDefault();
        scrollLocation = $( window ).scrollTop();
        window.location.hash = $( event.target ).attr('href').substr(1);
    });
    $( window ).on( "hashchange", function( event ){
        $( window ).scrollTop( scrollLocation );
        if( typeof callback === "function" ){
            callback();
        }
    });
}
hashThis( $( ".myAnchor" ), function(){
    // do something useful!
});
Bedwarmer answered 20/11, 2013 at 13:59 Comment(1)
You do not need the hashchange, just scroll back immediatelyDepressant
B
2

Adding this here because the more relevant questions have all been marked as duplicates pointing here…

My situation is simpler:

  • user clicks the link (a[href='#something'])
  • click handler does: e.preventDefault()
  • smoothscroll function: $("html,body").stop(true,true).animate({ "scrollTop": linkoffset.top }, scrollspeed, "swing" );
  • then window.location = link;

This way, the scroll occurs, and there's no jump when the location is updated.

Bunnie answered 18/7, 2014 at 8:38 Comment(0)
P
2

Erm I have a somewhat crude but definitely working method.
Just store the current scroll position in a temp variable and then reset it after changing the hash. :)

So for the original example:

$("#buttons li a").click(function(){
        $("#buttons li a").removeClass('selected');
        $(this).addClass('selected');

        var scrollPos = $(document).scrollTop();
        document.location.hash=$(this).attr("id")
        $(document).scrollTop(scrollPos);
});
Pollute answered 24/8, 2014 at 17:45 Comment(1)
The problem with this method is with Mobile Browsers you can notice that the scrolling is modifiedKif
F
1

if you use hashchange event with hash parser, you can prevent default action on links and change location.hash adding one character to have difference with id property of an element

$('a[href^=#]').on('click', function(e){
    e.preventDefault();
    location.hash = $(this).attr('href')+'/';
});

$(window).on('hashchange', function(){
    var a = /^#?chapter(\d+)-section(\d+)\/?$/i.exec(location.hash);
});
Froehlich answered 10/9, 2013 at 19:54 Comment(1)
Perfect! After five hours trying to fix the problem with the hash reload... :/ This was the only thing that worked!! Thanks!!!Hendon
E
1
  1. Save scroll position before changing url fragment.
  2. Change url fragment.
  3. Restore old scroll position.
let oldScrollPosition = window.scrollY;
window.location.hash = addressFragment;
window.scrollTo(0, oldScrollPosition);

It's fast, so client won't notice anything.

Ezraezri answered 5/9, 2022 at 19:34 Comment(1)
I've implemented this and it works well. Don't know why I didn't think of it myself.Invalidism
B
1

This worked for me using replaceState:

$('a[href^="#"]').click(function(){
    history.replaceState({}, '', location.toString().replace(/#.*$/, '') + $(this).attr('href'));
});
Boodle answered 27/9, 2022 at 2:7 Comment(0)
Q
0

I don't think this is possible. As far as I know, the only time a browser doesn't scroll to a changed document.location.hash is if the hash doesn't exist within the page.

This article isn't directly related to your question, but it discusses typical browser behavior of changing document.location.hash

Quelpart answered 28/9, 2009 at 22:12 Comment(1)
In my case the hash is added to url, and the hash exists, but it stopped scrolling down to the section as I want it to. Ctrl + L & Enter refresh works though... I wonder why?Dryly
H
0

The other way to do this is to add a div that's hidden at the top of the viewport. This div is then assigned the id of the hash before the hash is added to the url....so then you don't get a scroll.

Hebetic answered 3/2, 2011 at 12:18 Comment(0)
G
0

Here's my solution for history-enabled tabs:

    var tabContainer = $(".tabs"),
        tabsContent = tabContainer.find(".tabsection").hide(),
        tabNav = $(".tab-nav"), tabs = tabNav.find("a").on("click", function (e) {
                e.preventDefault();
                var href = this.href.split("#")[1]; //mydiv
                var target = "#" + href; //#myDiv
                tabs.each(function() {
                    $(this)[0].className = ""; //reset class names
                });
                tabsContent.hide();
                $(this).addClass("active");
                var $target = $(target).show();
                if ($target.length === 0) {
                    console.log("Could not find associated tab content for " + target);
                } 
                $target.removeAttr("id");
                // TODO: You could add smooth scroll to element
                document.location.hash = target;
                $target.attr("id", href);
                return false;
            });

And to show the last-selected tab:

var currentHashURL = document.location.hash;
        if (currentHashURL != "") { //a tab was set in hash earlier
            // show selected
            $(currentHashURL).show();
        }
        else { //default to show first tab
            tabsContent.first().show();
        }
        // Now set the tab to active
        tabs.filter("[href*='" + currentHashURL + "']").addClass("active");

Note the *= on the filter call. This is a jQuery-specific thing, and without it, your history-enabled tabs will fail.

Grimaud answered 8/3, 2014 at 20:51 Comment(0)
S
0

This solution creates a div at the actual scrollTop and removes it after changing hash:

$('#menu a').on('click',function(){
    //your anchor event here
    var href = $(this).attr('href');
    window.location.hash = href;
    if(window.location.hash == href)return false;           
    var $jumpTo = $('body').find(href);
    $('body').append(
        $('<div>')
            .attr('id',$jumpTo.attr('id'))
            .addClass('fakeDivForHash')
            .data('realElementForHash',$jumpTo.removeAttr('id'))
            .css({'position':'absolute','top':$(window).scrollTop()})
    );
    window.location.hash = href;    
});
$(window).on('hashchange', function(){
    var $fakeDiv = $('.fakeDivForHash');
    if(!$fakeDiv.length)return true;
    $fakeDiv.data('realElementForHash').attr('id',$fakeDiv.attr('id'));
    $fakeDiv.remove();
});

optional, triggering anchor event at page load:

$('#menu a[href='+window.location.hash+']').click();
Staphylococcus answered 27/3, 2014 at 13:21 Comment(0)
G
0

I have a simpler method that works for me. Basically, remember what the hash actually is in HTML. It's an anchor link to a Name tag. That's why it scrolls...the browser is attempting to scroll to an anchor link. So, give it one!

  1. Right under the BODY tag, put your version of this:
 <a name="home"></a><a name="firstsection"></a><a name="secondsection"></a><a name="thirdsection"></a>
  1. Name your section divs with classes instead of IDs.

  2. In your processing code, strip off the hash mark and replace with a dot:

    var trimPanel = loadhash.substring(1);    //lose the hash

    var dotSelect = '.' + trimPanel;  //replace hash with dot

    $(dotSelect).addClass("activepanel").show();        //show the div associated with the hash.

Finally, remove element.preventDefault or return: false and allow the nav to happen. The window will stay at the top, the hash will be appended to the address bar url, and the correct panel will open.

Gemperle answered 17/11, 2015 at 19:38 Comment(0)
A
0

I think you need to reset scroll to its position before hashchange.

$(function(){
    //This emulates a click on the correct button on page load
    if(document.location.hash) {
        $("#buttons li a").removeClass('selected');
        s=$(document.location.hash).addClass('selected').attr("href").replace("javascript:","");
        eval(s);
    }

    //Click a button to change the hash
    $("#buttons li a").click(function() {
            var scrollLocation = $(window).scrollTop();
            $("#buttons li a").removeClass('selected');
            $(this).addClass('selected');
            document.location.hash = $(this).attr("id");
            $(window).scrollTop( scrollLocation );
    });
});
Acrosstheboard answered 16/4, 2017 at 12:20 Comment(0)
B
0

If on your page you use id as sort of an anchor point, and you have scenarios where you want to have users to append #something to the end of the url and have the page scroll to that #something section by using your own defined animated javascript function, hashchange event listener will not be able to do that.

If you simply put a debugger immediate after hashchange event, for example, something like this(well, I use jquery, but you get the point):

$(window).on('hashchange', function(){debugger});

You will notice that as soon as you change your url and hit the enter button, the page stops at the corresponding section immediately, only after that, your own defined scrolling function will get triggered, and it sort of scrolls to that section, which looks very bad.

My suggestion is:

  1. do not use id as your anchor point to the section you want to scroll to.

  2. If you must use ID, like I do. Use 'popstate' event listener instead, it will not automatically scroll to the very section you append to the url, instead, you can call your own defined function inside the popstate event.

    $(window).on('popstate', function(){myscrollfunction()});

Finally you need to do a bit trick in your own defined scrolling function:

    let hash = window.location.hash.replace(/^#/, '');
    let node = $('#' + hash);
    if (node.length) {
        node.attr('id', '');
    }
    if (node.length) {
        node.attr('id', hash);
    }

delete id on your tag and reset it.

This should do the trick.

Boehmenism answered 4/5, 2018 at 21:38 Comment(0)
F
0

Here's my take on this (14 years after asked originally). I needed:

My approach is:

  1. Use location.replace() but save the current scroll position just before that;
  2. Restore the saved scroll position (just once) either in window hashchange event handler, or document scroll event handler – whichever occurs first.

The following example does that and implements custom scrolling (that could be omitted for the exact OP's purpose):

<!DOCTYPE html>

<style>
  table { border-collapse: collapse }
  td { border: thin solid;
       min-width: 500px; height: 250px;
       text-align: center;
       vertical-align: middle }
  td:target { outline: medium dashed green;
              outline-offset: 2px }
</style>

<table>
  <tr>
    <td id="1-1">1-1</td>
    <td id="1-2">1-2</td>
    <td id="1-3">1-3</td>
    <td id="1-4">1-4</td>
  </tr>
  <tr>
    <td id="2-1">2-1</td>
    <td id="2-2">2-2</td>
    <td id="2-3">2-3</td>
    <td id="2-4">2-4</td>
  </tr>
  <tr>
    <td id="3-1">3-1</td>
    <td id="3-2">3-2</td>
    <td id="3-3">3-3</td>
    <td id="3-4">3-4</td>
  </tr>
  <tr>
    <td id="4-1">4-1</td>
    <td id="4-2">4-2</td>
    <td id="4-3">4-3</td>
    <td id="4-4">4-4</td>
  </tr>
  <tr>
    <td id="5-1">5-1</td>
    <td id="5-2">5-2</td>
    <td id="5-3">5-3</td>
    <td id="5-4">5-4</td>
  </tr>
</table>

<script>
  (() => {
    let savedScroll;
    let scrollTarget;

    window.addEventListener("hashchange", scrollTargetCellIntoView);
    document.addEventListener("scroll", scrollTargetCellIntoView);
    document.querySelectorAll("td[id]").forEach(cell => {
      cell.onclick = evt => selectTargetCell(evt.target);
    });

    function replaceLocationHash(fragId) {
        // Doesn't update CSS :target
        //history.replaceState({}, "", "#" + cell.id);
        savedScroll = { y: window.scrollY,
                        x: window.scrollX };
        location.replace("#" + fragId);
    }

    function selectTargetCell(cell) {
      scrollTarget = cell;
      replaceLocationHash(cell.id);
    }

    function scrollTargetCellIntoView() {
      if (savedScroll) {
        window.scrollTo({ top: savedScroll.y,
                          left: savedScroll.x,
                          behavior: "instant" });
        savedScroll = null;
      }
      if (scrollTarget) {
        scrollTarget.scrollIntoView({ block: "nearest",
                                      inline: "nearest",
                                      behavior: "smooth" });
        scrollTarget = null;
      }
    }
  })();
</script>
Featherveined answered 13/9, 2023 at 8:38 Comment(0)
S
-5

Only add this code into jQuery on document ready

Ref : http://css-tricks.com/snippets/jquery/smooth-scrolling/

$(function() {
  $('a[href*=#]:not([href=#])').click(function() {
    if (location.pathname.replace(/^\//,'') == this.pathname.replace(/^\//,'') && location.hostname == this.hostname) {
      var target = $(this.hash);
      target = target.length ? target : $('[name=' + this.hash.slice(1) +']');
      if (target.length) {
        $('html,body').animate({
          scrollTop: target.offset().top
        }, 1000);
        return false;
      }
    }
  });
});
Spinthariscope answered 25/11, 2014 at 14:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.