Change URL hash on scroll and keep back button working

和自甴很熟 提交于 2021-01-27 07:15:48

问题


On a one page layout with fixed top menu and anchor navigation I have a "scrollspy" in place that changes the fragment identifier on scroll, gives the menu link an active class depending on scroll position and animates the scrolling to the anchor with Velocity.js.

Unfortunately what it also does, when clicking the browser back button it takes me through all the steps of the scrolled way, meaning I load the site and scroll down and up a tiny bit and then hit the back button frequently the browser will also scroll down and up but won't go to either the last visited id or back in browser history actually.

Here is the jsfiddle.

// jQuery on DOM ready

// In-Page Scroll Animation with VelocityJS
// ------------------------------------------------ //
// https://john-dugan.com/fixed-headers-with-hash-links/
$('.menu-a').on('click', function(e) {
    var hash  = this.hash,
        $hash = $(hash)

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

      $hash.velocity("scroll", { duration: 700, easing: [ .4, .21, .35, 1 ], complete: addHash });

    e.preventDefault();
});

// ScrollSpy for Menu items and Fragment Identifier
// ------------------------------------------------ //
// https://jsfiddle.net/mekwall/up4nu/
$menuLink           = $('.menu-a')

var lastId,
    // Anchors corresponding to menu items
    scrollItems = $menuLink.map(function(){
    var item = $($(this).attr("href"));
        if (item.length) { return item; }
    });


$(window).scroll(function(){
    // Get container scroll position
    var fromTop = $(this).scrollTop()+ 30; // or the value for the #navigation height

    // Get id of current scroll item
    var cur = scrollItems.map(function(){
        if ($(this).offset().top < fromTop)
        return this;
    });

    // Get the id of the current element
    cur = cur[cur.length-1];
    var id = cur && cur.length ? cur[0].id : "";
    if (lastId !== id) {
        lastId = id;

        // Set/remove active class
        $menuLink
        .parent().removeClass("active")
        .end().filter("[href='#"+id+"']").parent().addClass("active");
    }

    // If supported by the browser we can also update the URL
    // http://codepen.io/grayghostvisuals/pen/EtdwL
    if (window.history && window.history.pushState) {
        history.pushState("", document.title,'#'+id);
    }   
});

With the above code the following works fine:

  • The hash or fragment identifier updates fine when clicked on the menu link using VelocityJS for the scrolling animation.

  • The active class is given to the corresponding menu link on scrolling.

  • The fragment identifier also updates fine when scrolling instead of clicking the menu link.

Question
Part 1: When you scroll a tiny bit on the fiddle and then hit the back button you will see that the scrollbar "travels" the exact same way, remembering the scrolling that was done.

I need the back button to work like it normally does. a) Either go back in browser history and return to the page/site you were on and b) when having clicked an anchor link (i) and then anchor link (ii) and then the back button the page should go back to anchor link (i).

Part 2: Since history.pushState is not supported in IE8 I am looking for a way to use window.location.hash = $(this).attr('id'); instead. No matter what I have tried towards the end of the code I simply cannot get window.location.hash = $(this).attr('id'); to work. I don't really want to use HistoryJS or something for this but am interested to learn this and write it myself.

Apart from the back button broken behaviour all the other behaviour that I want is already there, now I just need to fix the back button behaviour.

edit I think I might have found a solution here, will test and then reply in detail if I get this to work.

Related:
smooth scrolling and get back button with popState on Firefox - need to click twice
jQuery in page back button scrolling
Modifying document.location.hash without page scrolling

How to Detect Browser Back Button event - Cross Browser


回答1:


To answer the first part of your question, if you don't want to pollute the browser's history, you can use history.replaceState() instead of history.pushState(). While pushState changes the URL and adds a new entry to the browser's history, replaceState changes the URL while modifying the current history entry instead of adding a new one.

There is also a good article including differences between pushState and replaceState on MDN.




回答2:


For older browsers I decided to include https://github.com/devote/HTML5-History-API and with this in place I got the desired behaviour (more or less).

This answers has:
- a scroll spy for the menu items and sets and active class to those on scroll
- the scroll spy also works for the URL hash, setting the correct hash depending on the section that is currently scrolled to
- a scroll stop function that checks when the user has stopped scrolling and then takes the value form the currently active menu item and sets that as the current URL hash. This is done on purpose to not catch the sections' anchors while scrolling but only the anchor of the section that the user scrolls to.
- a smooth scroll with Velocity.js when clicking on the menu links as well as when using the back and forward buttons
- functions that reacts to loading and reloading the page, meaning if you enter the page with a specific URL hash for a section it will animate the scroll to that section and if the page is reloaded it will animate the scroll to the top of the current section

The code is a rough sketch and could possibly use a few tweaks, this is just for demo purpose. I think I am still a beginner to please point out obvious errors so that I can learn from those. All links to where code snippets come from are included as well.

// In-Page Scroll Animation to Menu Link on click
// ------------------------------------------------ //
// https://john-dugan.com/fixed-headers-with-hash-links/
// https://stackoverflow.com/questions/8355673/jquery-how-to-scroll-an-anchor-to-the-top-of-the-page-when-clicked
// http://stackoverflow.com/a/8579673/1010918
// $('a[href^="#"]').on('click', function(e) {
$('.menu-a').on('click', function(e) {

    // default = make hash appear right after click
    // not default = make hash appear after scrolling animation is finished
    e.preventDefault();

    var hash  = this.hash,
        $hash = $(hash)

    $hash.velocity("scroll", { duration: 700, easing: [ .4, .21, .35, 1 ], queue: false });
});



// In-Page Scroll Animation to Hash on Load and Reload
// ----------------------------------------------------------- //
// https://stackoverflow.com/questions/680785/on-window-location-hash-change
// hashchange triggers popstate !
$(window).on('load', function(e) {

    var hash  = window.location.hash;
    console.log('hash on window load '+hash);
    $hash = $(hash)

    $hash.velocity("scroll", { duration: 500, easing: [ .4, .21, .35, 1 ], queue: false });

    // if not URL hash is present = root, go to top of page on reload
    if (!window.location.hash){
        $('body').velocity("scroll", { duration: 500, easing: [ .4, .21, .35, 1 ], queue: false });
    }   
});



// In-Page Scroll Animation to Hash on Back or Forward Button
// ---------------------------------------------------------- //
$('.menu-a').on('click', function(e) {  
    e.preventDefault(); 
    // keep the link in the browser history
    // set this separately from scrolling animation so it works in IE8
    history.pushState(null, null, this.href);
    return false
}); 
$(window).on('popstate', function(e) {
    // alert('popstate fired');
    $('body').append('<div class="console1">popstate fired</div>');
    $('.console1').delay(1000).fadeOut('slow');

    if (!window.location.hash){
        $('body').append('<div class="console2">no window location hash present</div>');

        $('body').velocity("scroll", { duration: 700, easing: [ .4, .21, .35, 1 ], queue: false });

        $('.console2').delay(1000).fadeOut('slow');
    }

    console.log('window.location.hash = '+window.location.hash);
    var hash  = window.location.hash;
    $hash = $(hash)

    $hash.velocity("scroll", { duration: 700, easing: [ .4, .21, .35, 1 ], queue: false });
});



// ScrollSpy for Menu items only - gives selected Menu items the active class
// ------------------------------------------------------------------------ //
// Does not update fragment identifier in URL https://en.wikipedia.org/wiki/Fragment_identifier
// https://jsfiddle.net/mekwall/up4nu/
    var lastId,

    // Anchors corresponding to menu items
    scrollItems = $menuLink.map(function(){
        var item = $($(this).attr("href"));
        // console.table(item);
        if (item.length) { return item; }
    });

    // Give menu item the active class on load of corresponding item
    function scrollSpy () {

        // Get container scroll position
        var fromTop = $(this).scrollTop()+ $menuButtonHeight;

        // Get id of current scroll item
        var cur = scrollItems.map(function(){
            if ($(this).offset().top < fromTop)
            return this;
        });

        // Get the id of the current element
        cur = cur[cur.length - 1];
        var id = cur && cur.length ? cur[0].id : "";

        if (lastId !== id) {
            lastId = id;
            // Set/remove active class
            $menuLink
            .parent().removeClass("active").end()
            .filter("[href='#"+id+"']").parent().addClass("active");
        }

        // Active Menu Link href Attribute
        activeMenuLinkHref = $('.menu-li.active > .menu-a').attr('href');
        // console.log('activeMenuLinkHref '+activeMenuLinkHref);   
    }
    scrollSpy()

    $(window).scroll(function(e){
        scrollSpy()
    });



// On Stop of Scrolling get Active Menu Link Href and set window.location.hash
// --------------------------------------------------------------------------- //

// Scroll Stop Function
//---------------------//
// https://stackoverflow.com/questions/8931605/fire-event-after-scrollling-scrollbars-or-mousewheel-with-javascript
// http://stackoverflow.com/a/8931685/1010918
// http://jsfiddle.net/fbSbT/1/
// http://jsfiddle.net/fbSbT/87/

(function(){ 
    var special = jQuery.event.special,
        uid1 = 'D' + (+new Date()),
        uid2 = 'D' + (+new Date() + 1); 
    special.scrollstart = {
        setup: function() { 
            var timer,
                handler =  function(evt) { 
                    var _self = this,
                        _args = arguments; 
                    if (timer) {
                        clearTimeout(timer);
                    } else {
                        evt.type = 'scrollstart';
                        // throws "TypeError: jQuery.event.handle is undefined"
                        // jQuery.event.handle.apply(_self, _args);
                        // solution
                        // http://stackoverflow.com/a/20809936/1010918
                        // replace jQuery.event.handle.apply with jQuery.event.dispatch.apply
                        jQuery.event.dispatch.apply(_self, _args);
                    } 
                    timer = setTimeout( function(){
                        timer = null;
                    }, special.scrollstop.latency); 
                }; 
            jQuery(this).bind('scroll', handler).data(uid1, handler); 
        },
        teardown: function(){
            jQuery(this).unbind( 'scroll', jQuery(this).data(uid1) );
        }
    }; 
    special.scrollstop = {
        latency: 250,
        setup: function() { 
            var timer,
                    handler = function(evt) { 
                    var _self = this,
                        _args = arguments; 
                    if (timer) {
                        clearTimeout(timer);
                    }
                     timer = setTimeout( function(){ 
                        timer = null;
                        evt.type = 'scrollstop';                        
                        // throws "TypeError: jQuery.event.handle is undefined"
                        // jQuery.event.handle.apply(_self, _args);
                        // solution
                        // http://stackoverflow.com/a/20809936/1010918
                        // replace jQuery.event.handle.apply with jQuery.event.dispatch.apply
                        jQuery.event.dispatch.apply(_self, _args); 
                    }, special.scrollstop.latency); 
                }; 
            jQuery(this).bind('scroll', handler).data(uid2, handler); 
        },
        teardown: function() {
            jQuery(this).unbind( 'scroll', jQuery(this).data(uid2) );
        }
    };

})();



// Scroll Stop Function Called
//----------------------------//

$(window).on("scrollstop", function() {

    // window.history.pushState(null, null, activeMenuLinkHref);
    // window.history.replaceState(null, null, activeMenuLinkHref);

    // http://stackoverflow.com/a/1489802/1010918 //
    // works best really
    hash = activeMenuLinkHref.replace( /^#/, '' );
    console.log('hash '+hash);
    var node = $( '#' + hash );
    if ( node.length ) {
      node.attr( 'id', '' );
      // console.log('node.attr id'+node.attr( 'id', '' ));
    }
    document.location.hash = hash;
    if ( node.length ) {
      node.attr( 'id', hash );
    }
});

CSS

.console1{
    position: fixed;
    z-index: 9999;
    top:0;
    right:0;    
    background-color: #fff;
    border: 2px solid red;
}

.console2{
    position: fixed;
    z-index: 9999;
    bottom:0;
    right:0;    
    background-color: #fff;
    border: 2px solid red;
}

I will also supply a jsfiddle in due time. ;)



来源:https://stackoverflow.com/questions/37419183/change-url-hash-on-scroll-and-keep-back-button-working

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!