davist11 / jQuery-One-Page-Nav

Smooth scrolling and smart navigation when user scrolls on one-page sites.
http://davist11.github.com/jQuery-One-Page-Nav/
MIT License
1.52k stars 501 forks source link

Scroll offset etc. #171

Closed Seefahrer closed 5 years ago

Seefahrer commented 5 years ago

Hi, first of all I'm a real fan of your "onepagenav" plugin and I'm using it on my website: https://www.luetten-dieks-carlito.eu ... in conjunction with the "Contao 4.7.x" CMS.

Due to the layout of my site with alternating backgrounds, I cannot use your favoured CSS solution for an offset of the content section to navigate to. So, I tried to revoke your JS offset withdrawel by modifying yr script to:

/*
 * jQuery One Page Nav Plugin
 * http://github.com/davist11/jQuery-One-Page-Nav
 *
 * Copyright (c) 2010 Trevor Davis (http://trevordavis.net)
 * Dual licensed under the MIT and GPL licenses.
 * Uses the same license as jQuery, see:
 * http://jquery.org/license
 *
 * @version 3.0.0
 *
 * Example usage:
 * $('#nav').onePageNav({
 *   currentClass: 'active',
 *   changeHash: false,
 *   scrollSpeed: 1000,
 *   scrollOffset: 80
 * });
 */

;(function($, window, document, undefined){

    // our plugin constructor
    var OnePageNav = function(elem, options){
        this.elem = elem;
        this.$elem = $(elem);
        this.options = options;
        this.metadata = this.$elem.data('plugin-options');
        this.$win = $(window);
        this.sections = {};
        this.didScroll = false;
        this.$doc = $(document);
        this.docHeight = this.$doc.height();
    };

    // the plugin prototype
    OnePageNav.prototype = {
        defaults: {
            navItems: 'a',
            currentClass: 'active',
            changeHash: false,
            easing: 'swing',
            filter: '',
            scrollSpeed: 750,
            scrollOffset: 0,
            scrollThreshold: 0.5,
            begin: false,
            end: false,
            scrollChange: false
        },

        init: function() {
            // Introduce defaults that can be extended either
            // globally or using an object literal.
            this.config = $.extend({}, this.defaults, this.options, this.metadata);

            this.$nav = this.$elem.find(this.config.navItems);

            //Filter any links out of the nav
            if(this.config.filter !== '') {
                this.$nav = this.$nav.filter(this.config.filter);
            }

            //Handle clicks on the nav
            this.$nav.on('click.onePageNav', $.proxy(this.handleClick, this));

            //Get the section positions
            this.getPositions();

            //Handle scroll changes
            this.bindInterval();

            //Update the positions on resize too
            this.$win.on('resize.onePageNav', $.proxy(this.getPositions, this));

            return this;
        },

        adjustNav: function(self, $parent) {
            self.$elem.find('.' + self.config.currentClass).removeClass(self.config.currentClass);
            $parent.addClass(self.config.currentClass);
        },

        bindInterval: function() {
            var self = this;
            var docHeight;

            self.$win.on('scroll.onePageNav', function() {
                self.didScroll = true;
            });

            self.t = setInterval(function() {
                docHeight = self.$doc.height();

                //If it was scrolled
                if(self.didScroll) {
                    self.didScroll = false;
                    self.scrollChange();
                }

                //If the document height changes
                if(docHeight !== self.docHeight) {
                    self.docHeight = docHeight;
                    self.getPositions();
                }
            }, 250);
        },

        getHash: function($link) {
            return $link.attr('href').split('#')[1];
        },

        getPositions: function() {
            var self = this;
            var linkHref;
            var topPos;
            var $target;

            self.$nav.each(function() {
                linkHref = self.getHash($(this));
                $target = $('#' + linkHref);

                if($target.length) {
                    topPos = $target.offset().top;
                    self.sections[linkHref] = Math.round(topPos) - this.config.scrollOffset;
                }
            });
        },

        getSection: function(windowPos) {
            var returnValue = null;
            var windowHeight = Math.round(this.$win.height() * this.config.scrollThreshold);

            for(var section in this.sections) {
                if((this.sections[section] - windowHeight) < windowPos) {
                    returnValue = section;
                }
            }

            return returnValue;
        },

        handleClick: function(e) {
            var self = this;
            var $link = $(e.currentTarget);
            var $parent = $link.parent();
            var newLoc = '#' + self.getHash($link);

            if(!$parent.hasClass(self.config.currentClass)) {
                //Start callback
                if(self.config.begin) {
                    self.config.begin();
                }

                //Change the highlighted nav item
                self.adjustNav(self, $parent);

                //Removing the auto-adjust on scroll
                self.unbindInterval();

                //Scroll to the correct position
                self.scrollTo(newLoc, function() {
                    //Do we need to change the hash?
                    if(self.config.changeHash) {
                        window.location.hash = newLoc;
                    }

                    //Add the auto-adjust on scroll back in
                    self.bindInterval();

                    //End callback
                    if(self.config.end) {
                        self.config.end();
                    }
                });
            }

            e.preventDefault();
        },

        scrollChange: function() {
            var windowTop = this.$win.scrollTop();
            var position = this.getSection(windowTop);
            var $parent;

            //If the position is set
            if(position !== null) {
                $parent = this.$elem.find('a[href$="#' + position + '"]').parent();

                //If it's not already the current section
                if(!$parent.hasClass(this.config.currentClass)) {
                    //Change the highlighted nav item
                    this.adjustNav(this, $parent);

                    //If there is a scrollChange callback
                    if(this.config.scrollChange) {
                        this.config.scrollChange($parent);
                    }
                }
            }
        },

        scrollTo: function(target, callback) {
            var offset = $(target).offset().top - this.config.scrollOffset;
            console.log(offset);
            $('html, body').animate({scrollTop: offset}, this.config.scrollSpeed, this.config.easing, callback);
        },

        unbindInterval: function() {
            clearInterval(this.t);
            this.$win.unbind('scroll.onePageNav');
        }
    };

    OnePageNav.defaults = OnePageNav.prototype.defaults;

    $.fn.onePageNav = function(options) {
        return this.each(function() {
            new OnePageNav(this, options).init();
        });
    };

})( jQuery, window , document );

Your script is loaded and initialized as a Contao-Template with fllwg declaration:

<script src="<?= TL_FILES_URL ?>files/freestyle-fx/js/jquery.nav.min.js"></script>
<script>
jQuery(function($) { 
    $('#nav').onePageNav({
        currentClass: 'active',
        scrollSpeed: 1000,
        changeHash: true,
        easing: 'easeInOutCirc',
        scrollOffset: $('#header-bar').height(),
    begin: function() {
        //Hack so you can click other menu items after the initial click
        $('body').append('<div id="device-dummy" style="height: 1px;"></div>');
    },
    end: function() {
        $('#device-dummy').remove();
    }
    });
});
</script>

The modified version works so far as expected on Mac/Safari, on e.g. Firefox, Chrome, Opera I notice some strange behaviour: The script first scrolls as it should to the offset of the section to navigate to, but then jumps to Top-Position ... i.e. as if I'd given the offset=0 ... Do you have an explanation or workaraound for that?

As I'm using the "headroom.js" navigation, I furthermore would like to change the offset value dynamically depending on the scroll-direction, i.e. 80px for "up" ($("header-bar".height())because the header-bar will slide in, and "0" for "down" because the header-bar will disappear. This would be easy to accomplish with CSS but, as aforesaid, this approach will not work ...

Btw: Your "IOS-Hack" ist also necessary and does work in MacOS Mojave ...

davist11 commented 5 years ago

You can use the CSS solution regardless of your design.

Seefahrer commented 5 years ago

Unfortunately that does not work ... If a assign a classname ".section" to all of my content-sections, the css:

.section {
    margin-top: -80px;
    padding-top: 80px;
}

causes the blue background of sections with a blue background to protude into the sections before ... Ok, bottom line, it seems that I have to live with that, because all former questions relating to that issue were answered with: do it with css ...

davist11 commented 5 years ago

That would be an outer wrapper. You would declare any background colors in a wrapper inside of that one.

Seefahrer commented 5 years ago

You have of course much more forgotten about html, php, js, css than I will ever know, but I do not get it to work with CSS... But I meanwhile understand that I never will get an answer to any question related to a JS solution ...

davist11 commented 5 years ago

Sorry, I just don't have the bandwidth to support questions about custom javascript

Seefahrer commented 5 years ago

Dear Trevor, I really understand that you cannot work free of charge on every single customer request ... sorry that I bother you again ... But please let me ask one last question: I need your script always to scroll to a position 80 px below the upper edge of the screen. Under Safari that's not a problem, but Chrome, FF, Opera etc. just scroll to that 80px position, but then jump suddenly to 0px position. It seems, that there is some function in your script that adjusts always the topPos to "0" after the "scrollTo"-function. Could you pls assist me in preventing such behaviour? Many thanks in advance ... https://www.luetten-dieks-carlito.eu

davist11 commented 5 years ago

That's exactly why the CSS solution exists to offset horizontal navigations http://davist11.github.io/jQuery-One-Page-Nav/top.html

zfyre123 commented 5 years ago

Had a similar issue, came up with this, hope it helps:

$('header .link-move a').click(function(){ $('html, body').animate({ scrollTop: $( $.attr(this, 'href') ).offset().top - 80 }); return false; });

Added the class to which one needs the offset.

Seefahrer commented 5 years ago

Hi @zfyre123 ... yours is not really solving the issue, but the modified script attached which accepts an offset via options. The blamed behaviour of browsers like Chrome or FF can be bypassed by a statement eg:

if(self.config.changeHash) {
                        if(history.pushState) {
                            history.pushState(null, null, pathName + newLoc);
                        }
                        else {
                            window.location.hash = pathName + newLoc;
                        }
}

Then the page scrolls smoothly exactly to the desired offset.

It further more makes the script applicable for multilanguage sites because it takes care of prepending locales in the browser address bar. Pls check it on my site: https://www.luetten-dieks-carlito.eu/en/#about-me etc. May be it helps someone else with the same problems. Now I'm totally satisfied with Trevor's script!

/*
 * jQuery One Page Nav Plugin
 * http://github.com/davist11/jQuery-One-Page-Nav
 *
 * Copyright (c) 2010 Trevor Davis (http://trevordavis.net)
 * Dual licensed under the MIT and GPL licenses.
 * Uses the same license as jQuery, see:
 * http://jquery.org/license
 *
 * @version 3.0.0
 *
 * Example usage:
 * $('#nav').onePageNav({
 *   currentClass: 'active',
 *   changeHash: false,
 *   scrollSpeed: 1000,
 *   scrollOffset: 80
 * });
 */

;(function($, window, document, undefined){

    // our plugin constructor
    var OnePageNav = function(elem, options){
        this.elem = elem;
        this.$elem = $(elem);
        this.options = options;
        this.metadata = this.$elem.data('plugin-options');
        this.$win = $(window);
        this.sections = {};
        this.didScroll = false;
        this.$doc = $(document);
        this.docHeight = this.$doc.height();    
    };

    // get the locale prefix i.e. either 'en', 'de" or else
    pathName = window.location.pathname;

    // the plugin prototype
    OnePageNav.prototype = {
        defaults: {
            navItems: 'a',
            currentClass: 'active',
            changeHash: false,
            easing: 'swing',
            filter: null,
            scrollSpeed: 750,
            scrollOffset: 0,
            scrollThreshold: 0.5,
            begin: false,
            end: false,
            scrollChange: false
        },

        init: function() {
            // Introduce defaults that can be extended either
            // globally or using an object literal.
            this.config = $.extend({}, this.defaults, this.options, this.metadata);

            this.$nav = this.$elem.find(this.config.navItems);

            //Filter any links out of the nav
            if(this.config.filter) {
                this.$nav = this.$nav.filter(this.config.filter);
            }

            //Handle clicks on the nav
            this.$nav.on('click.onePageNav', $.proxy(this.handleClick, this));

            //Get the section positions
            this.getPositions();

            //Handle scroll changes
            this.bindInterval();

            //Update the positions on resize too
            this.$win.on('resize.onePageNav', $.proxy(this.getPositions, this));

            return this;
        },

        adjustNav: function(self, $parent) {
            self.$elem.find('.' + self.config.currentClass).removeClass(self.config.currentClass);
            $parent.addClass(self.config.currentClass);
            var link= $parent[0].firstElementChild.hash;
            if(self.config.changeHash) {
                if(history.pushState) {
                    history.pushState(null, null, pathName + link);
                }
                else {
                    location.hash = pathName + link;
                }
            }
        },

        bindInterval: function() {
            var self = this;
            var docHeight;

            self.$win.on('scroll.onePageNav', function() {
                self.didScroll = true;
            });

            self.t = setInterval(function() {
                docHeight = self.$doc.height();

                //If it was scrolled
                if(self.didScroll) {
                    self.didScroll = false;
                    self.scrollChange();
                }

                //If the document height changes
                if(docHeight !== self.docHeight) {
                    self.docHeight = docHeight;
                    self.getPositions();
                }
            }, 250);
        },

        getHash: function($link) {
            return $link.attr('href') ? $link.attr('href').split('#')[1] : '';
        },

        getPositions: function() {
            var self = this;
            var linkHref;
            var topPos;
            var $target;

            self.$nav.each(function() {
                linkHref = self.getHash($(this));
                $target = linkHref ? $('#' + linkHref) : '';;

                if($target.length) {
                    topPos = $target.offset().top;
                    self.sections[linkHref] = Math.round(topPos) - self.config.scrollOffset;
                }
            });
        },

        getSection: function(windowPos) {
            var returnValue = null;
            var windowHeight = Math.round(this.$win.height() * this.config.scrollThreshold);

            for(var section in this.sections) {
                if((this.sections[section] - windowHeight) < windowPos) {
                    returnValue = section;
                }
            }

            return returnValue;
        },

        handleClick: function(e) {
            var self = this;
            var $link = $(e.currentTarget);
            var $parent = $link.parent();
            var hash = self.getHash($link);
            var newLoc = '#' + hash;

            if (!hash || self.isExternalLink($link) || $(newLoc).length === 0) {
                return;
            }

            if(!$parent.hasClass(self.config.currentClass)) {
                //Start callback
                if(self.config.begin) {
                    self.config.begin();
                }

                //Change the highlighted nav item
                self.adjustNav(self, $parent);

                //Removing the auto-adjust on scroll
                self.unbindInterval();

                //Scroll to the correct position
                self.scrollTo(newLoc, function() {
                    //Do we need to change the hash?

                    if(self.config.changeHash) {
                        if(history.pushState) {
                            history.pushState(null, null, pathName + newLoc);
                        }
                        else {
                            window.location.hash = pathName + newLoc;
                        }
                    }

                    //Add the auto-adjust on scroll back in
                    self.bindInterval();

                    //End callback
                    if(self.config.end) {
                        self.config.end();
                    }
                });
            }

            e.preventDefault();
        },

        scrollChange: function() {
            var windowTop = this.$win.scrollTop();
            var position = this.getSection(windowTop);
            var $parent;

            //If the position is set
            if(position !== null) {
                $parent = this.$elem.find('a[href$="#' + position + '"]').parent();

                //If it's not already the current section
                if(!$parent.hasClass(this.config.currentClass)) {
                    //Change the highlighted nav item
                    this.adjustNav(this, $parent);

                    //If there is a scrollChange callback
                    if(this.config.scrollChange) {
                        this.config.scrollChange($parent);
                    }
                }
            }
        },

        scrollTo: function(target, callback) {
            var offset = $(target).offset().top - this.config.scrollOffset;
            $('html, body').animate({scrollTop: offset}, this.config.scrollSpeed, this.config.easing, callback);
        },

        unbindInterval: function() {
            clearInterval(this.t);
            this.$win.unbind('scroll.onePageNav');
        },

        isExternalLink: function($link) {
            return window.location.host !== $link.get(0).host;
        }
    };

    OnePageNav.defaults = OnePageNav.prototype.defaults;

    $.fn.onePageNav = function(options) {
        return this.each(function() {
            new OnePageNav(this, options).init();
        });
    };

})( jQuery, window , document );