preface

This is an old-fashioned requirement, an interaction requirement that has been popular since the JQ era. There are also a lot of information on the network, I write the purpose of this article is more mainly aimed at the insufficiency of these information, to solve some problems:

  • The data for top – lifting and scrolling navigation are independent and analyzed as separate functions. I’m going to analyze the top suction function as one of the features of the scroll navigation, as a whole package.
  • The analysis of top suction and scrolling navigation is mostly written from the standpoint of top navigation, but in fact, this function can appear at any position on the page, and the plan written by these data lacks the extension of the scene. Since we are going to extend it, a lot of details need to be considered.
  • The introduction of scrolling navigation is often on traditional listeningscrollMethods andposition: stickyMethod independent introduction, let the reader choose one. Here, I’m going to blend the two methods and let the browser decide which method to use, and try to take the simplest optimized onestickyMethods.
  • Practice and uncoverstickyThe veil of mystery, gives an insight that differs from many sources, and points out some misleading descriptions.
  • stickyAnd the traditional way of combining.

First look at the overall effect and the practical goal of this time, this article has implemented the effect of changing the map tutorial, to understand the scroll navigation + top of the scheme to be expressed, so from the practice of triggering the explanation, than a pure abstract scheme to be easier to understand.

Since I did this again after many years, from the popular JQ to the era of more frameworks, I finally packaged a VUE component for you to choose from.

demand

Here is an example of a relatively complex point, let’s not stick to the top of the navigation bar of this effect, it is relatively simple to implement, in fact, this effect can appear in any part of the page, although the implementation is essentially the same, but need to pay attention to small details, or a lot of.

Our goal is to achieve the GIF effect in the foreword above

Let’s look at the HTML structure in the figure below

<html>
<body>
    <div class="left-section"></div>
    <div class="right-section">
        <div class="top-section"></div>
        <div class="nav-bar-wrap">
            <ul class="nav-bar">
                <li data-content="content1" class="active">Navigation 1</li>
                <li data-content="content2">Navigation 2</li>
                <li data-content="content3">The navigation 3</li>
            </ul>
        </div>
        <div class="nav-content-container">
            <div class="content content1">Navigation 1 content</div>
            <div class="content content2">Navigation 2 content</div>
            <div class="content content3">Navigation 3 content</div>
        </div>
    </div>
</body>
</html>
Copy the code

As you can see, the nav-bar is a location in the middle of the page.

Let’s take a look at some key CSS styles (summary)

html {
    height: 100%;
    overflow: hidden;
}
body {
    padding-top: 24px; /* That's the point */
    height: 100%;
    overflow: auto;
}

.left-section {
    display: inline-block;
    width: 300px;
}
.right-section {
    position: relative; /* That's the point */
    display: inline-block;
    margin-left: 24px;
    width: 700px;
}
.nav-bar-wrap {
    margin: 16px 0;
    height: 55px; /* That's the point */
}
.nav-bar {
    height: 100%;
}
Copy the code

The above Settings, point to the effect

  • Mainly is to letbodyBecomes the scroll container, the element to which the scroll bar belongs.
  • left-sectionandright-sectionEach occupies the left and right sides of the page
  • The navigation bar.nav-barBy a parent elementnav-bar-wrapPackage! It’s for space! When the navigation bar.nav-barAfter suction top, setposition: fixedWithout the parent element, the content of the page would move up to fill the gap, and the page would not flow smoothly.
  • .right-sectionSet uppositionIt becomes the navigation content and navigation baroffsetParentIt’s not a rolling containerbodyThis is something to be aware of, andbodySet uppadding-top: 24px.

The above Settings create a more complex scrolling navigation situation.

Let’s put the demo out at the beginning, so we can look at all of the CSS, and we’ll move on to the JS section.

Traditional listen scroll method

So let’s just introduce the two methods separately and then merge them together. I’m going to focus on the traditional scroll method.

Ideas:

  • Listening to thescrollEvent, when the rolling distance reaches the condition to pull the top (based on the navigation baroffsetTopTo calculate and judge), the navigation bar is set toposition: fixed;
  • Records the contents corresponding to each navigationoffsetTop, generally when the scrolling distance is greater than or equal to the corresponding contentoffsetTop, set the selected state of the navigation bar;
  • Click the navigation bar navigation, set the scroll containerscrollTop, usually set to contentoffsetTop;

Here’s the simplest idea. Of course, there will be a lot of details to pay attention to, we will understand these details in the following step by step implementation:

  • When the container causing the scroll is not the corresponding content of the navigationoffsetParent(Explained lateroffsetParent), so in judgmentscrollTopWith the content ofoffsetTopWhen, some extra calculations are added.
  • When the navigation bar does not compare the top layer in the page DOM structure like the top navigation, and the width is typically as long as the screen width, such as the small navigation under a div in the page DOM tree, to do the top, due to the settingposition:fixed;If the width and height value is relative value, such as percentage, the relative benchmark has changed, it is necessary to deal with its width and height.
  • When the content on the page changes, such as loading data, rearranging and redrawing the page, it is necessary to update the navigation bar itself and the container in which each navigation content residesoffsetTopOtherwise, the subsequent calculation and judgment will be affected. This is more important, after all, now many pages are not static, not the whole page for the refresh, are local refresh.
  • When the browser screen changes (resize), so update the navigation bar itself and the container in which the content of each navigation is located because it may cause rearrangement and redrawingoffsetTopOtherwise, the subsequent calculation and judgment will be affected.
  • When the scrollbar reaches the bottom, the last navigation bar must be forcibly selected if there is no condition for the last navigation bar to be selected. This is a small optimization of the interaction.

offsetParent

Let’s explain what offsetParent is, because the element’s offsetTop is calculated based on the area inside the element’s offsetParent, including the padding.

Note the special case where offsetParent is body and offsetTop is calculated from the margin of the body. This is not mentioned in official sources, but I found it in practice.

The ancestor element nearest to an element whose position is relative, absolute, and sticky is the offsetParent of the element. If the ancestor element does not have this setting, The nearest TD,th,table, or body element is offsetParent.

The offsetParent of an element is null in three cases:

  • The element or its parent is setdisplay: none
  • The element is set by itselfposition:fixed(Firefox or back<body>)
  • The element is<body>or<html>

The solution

In order to make it easy to understand, the following is a simple and crude way to write.

Binding to monitor

First we define some of the variables we need

var navBar = document.querySelector('.nav-bar');
var menu = document.querySelectorAll('.nav-bar li');
var scrollContainer = document.querySelector('body');
var offsetTops = {}; // Store offsetTop for each part
Copy the code

For the scroll container, body in this case, do the key scroll event binding:

scrollContainer.addEventListener('scroll', handleScroll);
Copy the code

It is worth noting that if HTML is the container of scroll, binding the object that listens for Scroll is window, and using HTML binding will not work

window.addEventListener('scroll', handleScroll);
Copy the code

Next we get the offsetTop of the navigation bar and the contents of each navigation.

calcTop(true);
@param {Boolean} recalNav - Whether to calculate the navigation bar offsetTop */
function calcTop(recalNav) {
    recalNav && (offsetTops.navBar = navBar.offsetTop);
    ['content1'.'content2'.'content3'].forEach(item= > {
        offsetTops[item] = document.querySelector('. ' + item).offsetTop;
    });
}
Copy the code

Note that this method is called once whenever the content of the page changes (such as rendering data to HTML after a request to load data) to make sure the offsetTop value is up to date.

One of the key handleScroll is the focus, which we explain step by step:

function handleScroll() {
    var top = scrollContainer.scrollTop; // Get the current scroll bar scroll distance
    // This is the control navigation bar top,
    // Why subtract 24?
    / / because said, rolling container body not navigation and navigation content offsetParent, so scrollContainer. The distance of the scrollTop paddingTop begin to calculate from the body
    if ((top - 24) >= offsetTops.navBar) {
        navBar.style.position = 'fixed';
        navBar.style.top = 0;
        // Due to the change to 'fixed', the parent element is changed and the style has to be reset in order to maintain the original style
        navBar.style.left = '124px';
        navBar.style.width = '300px';
        navBar.style.height = '55px';
    }
    // This is control navigation bar top - cancel top
    if ((top - 24) < offsetTops.navBar) {
        navBar.style.position = 'static';
        navBar.style.width = '100%';
        navBar.style.height = '100%';
    }
    resetNavSelect();
    // This is the distance measured after the top. Why add 31?
    // In the case of no top, the navigation of the specified content can be achieved by scrolling to the top of the body, i.e. scrolling the distance of the content offsetTop + body paddingTop
    // But after the top, just scroll to the bottom of the top navigation bar to reach the specified navigation content, so the distance equivalent to scrolling [content offsetTop + body paddingTop - the height of the top navigation bar] will reach the critical value
    // To understand the formula, c represents the offsetTop of the navigation content, s represents the scrolling distance, body paddingTop is 24, and top navigation bar height is 55. As long as the rolling distance is greater than or equal to the critical value mentioned above, the corresponding navigation must be reached.
    // So the formula is: s >= c + 24-55, that is, when s + 31 >= c, the condition is valid, so the scrollTop of the rolling container should be added 31, which is the value to judge
    var fixedBaseTop = top + 31;
    var menuLength = menu.length;
    // When the scrollbar reaches the bottom, select the last navigation
    if (top + scrollContainer.clientHeight >= scrollContainer.scrollHeight) {
        menu[menuLength - 1].className = 'active';
        return;
    }
    // The following navigation is automatically selected based on scrolling
    // When the scroll content reaches the bottom of the navigation bar after the top, and the following navigation content does not reach the navigation bar, the navigation content is selected
    // Execution in the for loop does not include judging the last navigation
    for (var i = 0; i < menuLength - 1; i++) {
        if (fixedBaseTop >= offsetTops['content' + (i + 1)] && fixedBaseTop < offsetTops['content' + (i + 2)]) {
            menu[i].className = 'active';
            return; }}// Check the last navigation, if it has reached the bottom of the navigation bar, select it
    if (fixedBaseTop >= offsetTops['content' + (menuLength - 1)]) {
        menu[menuLength - 1].className = 'active';
        return;
    }
    // If none of the conditions match, select the first navigation.
    menu[0].className = 'active';
}
Copy the code

The code comment above illustrates what kind of bias calculation is required if the scroll container body is not the offsetParent of the navigation bar and navigation content. It is generally necessary to consider the distance between the scrolling container and the offsetParent of the navigation bar and navigation content, as well as the height of the navigation bar itself after the top of the navigation bar.

And when, after suction a top to make a supplement to the style of the navigation bar, the example here is relatively simple, absorb before the roof is a fixed value of px, after but when complex layout point of your navigation bar wide high itself is based on a percentage of the original, then later to take to suction a top navigation bar assignment.

We then add a listening event to recalculate the offsetTop of the navigation bar and navigation content for changing the page browser size

window.addEventListener('resize', hanldeResize);

function hanldeResize() {
    calcTop(true);
}
Copy the code

Because the browser size of the page changes, the content of the page may also change, causing the original offsetTop to become old and to be updated in time so that the scrolling navigation is properly compared.

Click to jump to the navigation content

At this point, the basic core logic has been handled. The last part is the easy part, which is to click on the navigation and scroll to the navigation content.

navBar.onclick = selectNav;
/** * select the title to jump to the corresponding content */
function selectNav(e) {
    var ev = e || event;
    var target = ev.target || ev.srcElement; / / compatible with IE
    this.resetNavSelect();
    target.className = 'active';
    scrollContainer.scrollTop = offsetTops[target.getAttribute('data-content')] - 31;
}
Copy the code

In the scrollTop assigned by the above method, 31 is subtracted, which is the deviation value explained above. Since the navigation is positioned according to this deviation value when scrolling, it is also calculated according to this deviation value when clicking the navigation bar to jump to the navigation content. Of course you have a personality, and you can be forgiven for thinking otherwise.

summary

This is the end of the introduction to traditional implementations. The above example is not optimized, but it is written to make it easier for you to understand, so you can see how it is written in the packaged VUE component later. Many of the details are also considered in the prop.

The CSS is new with sticky

Sticky is a value of position, which means sticky.

Ugly to say before, sticky compatibility is not very good, check here

To be honest, there are many materials about the introduction of this feature, but I think the description is not very accurate, especially the common description is between relative and fixed, which I think is very misleading.

Let’s start with a general statement:

To set upstickyWhen certain conditions are met, the element will produce viscosity, keeping its original position unchanged, as if it is stuck, while when the conditions are not met, it will have the same effect as ordinary.

So what are the conditions? Let me list them

Viscous condition

For the elements with sticky position set, I will describe them as “sticky positioning elements” in this paper. The so-called “sticky” will be generated only when the following conditions are met:

  • Be sure to set location properties (top/left/bottom/right)
  • Sticky positioning elements (excluding margins) and their most recent ancestorscrolling boxThe distance (including border and padding) is less than or equal to the set azimuth attribute threshold. If you don’t have a scrolling box, it calculates based on viewPort
  • How do you tell if you’ve reached the threshold, based on thatscrolling boxOf the rolling events determined, by implication, thatscrolling boxIt must be scrollable (in the direction of the orientation property you set, if you want to scroll vertically, the vertical direction must be scrollable), it will only work if scrollable is monitored (meaning if it is setoverflow: hiddenIs ineffectual), and will not be affected by other ancestorsscrolling boxThe rolling effect of.
  • The visible area of the parent element can hold sticky positioning elements. Typically occurs when the parent element is not a scroll container. This will be illustrated below.

Let’s summarize and simplify the description of the above conditions:

Sticky positioning elements must be in a scrollable container and must have a orientation property that is used as the closest ancestorscrolling boxIs less than or equal to. But under the influence of scrolling, if the visible area can’t accommodate the sticky positioning element, the viscosity will also disappear.

For the above mentioned scrolling box, this article will temporarily call it “scrolling container”.

Scrolling box: container that contains scrolling bars or has overflowCSS set. Note that overflow is set, either for hidden or for overflow in a single direction, such as overflow-x

performance

Let’s get to know some specific manifestations of sticky. After reading these, you will probably know how it is a process and the effect. (Note the bold)

When you set orientation properties such as top: 0, then by azimuth attribute value as a threshold, when the viscous positioning elements and rolling container in the border area of the distance (i.e., excluding margin) is equal to the threshold, the element will behave in a viscous, namely the position will not change, like a stick in that position, the following distance is less than the threshold, or that stick to the position of the moment.

Note stick refers to the position will not change, other styles, such as high or wide before, with particular emphasis on said this is because a lot of information will use fixed to the viscous behavior, in fact is not rigorous, truly become fixed, wide high percentage of style benchmark is becomes a page, but the viscosity behavior, wide high percentage is based on the parent element.

More importantly, stickiness also preserves the original document stream location, rather than leaving the document stream. So it makes sense that stickiness is stuck to the parent element, not the page.

When the viscosity doesn’t work

The behavior of sticky positioning elements is static, which is described as “relative” in many materials. However, in practice, it is closer to static, because the set orientation attribute has no effect before stickiness takes effect, while the orientation attribute of “relative” is effective.

When the stickiness takes effect

If the distance between the viscous positioning element and the nearest rolling container is less than the set azimuth attribute threshold, then the viscous effect will be produced when the rolling element becomes less than or equal to the threshold, which is shown as sticking to the position equal to the threshold.

However, if the distance between the viscous positioning element and the nearest rolling container is greater than the set threshold of orientation attribute, the setting of threshold is equivalent to playing a positioning role, such as top: 100px, the sticky positioning element (excluding margin) is immediately glued 100px away from the nearest scroll container (border,padding), but the sticky element is still on the parent element.

When the parent of a sticky positioning element is not a scroll container

Let’s take vertical scrolling as an example,

When viscous effect happens, continue to scroll down, the parent element will continue to be rolled up (behave) this is normal, if the sticky box model positioning element itself (including margin) when you arrive at the bottom of the parent element, then continue rolling viscous positioning elements will also be rolled up (from the Angle of view can be understood as viscous effect disappeared).

Because viscosity is relative to the region of the parent element, if the region of the parent element surrounding the viscous fixed element is completely rolled up, naturally, the localization element will also roll up with the parent element.

This is the case with the last of the conditions.

(For this part of the description, other articles will say that the height of the parent element is greater than the element, they say from the conclusion, I here from the nature of the phenomenon, it is clearer.)

Apply to this example

Now that we’ve covered sticky, it’s time to apply it to my example above. Sticky is only used to achieve the top sucking effect, so other functions (scrolling navigation, jumping navigation content, etc.) are still needed. The following only said to achieve the top effect part.

Or the original HTML and CSS based on the following CSS

.nav-bar-wrap {
    position: sticky;
    PaddingTop */
    top: -24px;
}
Copy the code

And you’re done! Isn’t it super simple

This section of CSS code is to replace the traditional solution of this section of JS code, and do not need to calculate the top after the style:

// This is the control navigation bar top - top
if ((top + extraFixed) >= offsetTops.navBar) {
    navBar.style.position = 'fixed';
    navBar.style.top = 0;
    navBar.style.left = '124px';
    navBar.style.width = '300px';
    navBar.style.height = '55px';
}
// This is control navigation bar top - cancel top
if ((top + extraFixed) < offsetTops.navBar) {
    navBar.style.position = 'static';
    navBar.style.width = '100%';
    navBar.style.height = '100%';
}
Copy the code

The combination

The combination of the two means that, according to whether the browser supports sticky, to judge the effect of using CSS or JS control roof.

But to be honest, unless you know exactly which browser your page is going to be in (or need to be in), then you can just write the appropriate piece of code.

But if you’re not sure what browser you’re going to use, or if you’re going to use most browsers, the combination of the two doesn’t save you a lot of code, which means you have to write both implementations and decide which one you’re actually going to use. The point of doing this is simply to use CSS where possible to minimize DOM manipulation, which is a performance optimization. If you don’t have that ambition, you can write traditional.

Add a method to check whether the browser supports sticky

var isSupportSticky = validateSticky();
/** * Check whether the browser supports the sticky value. If it does not return false, add the sticky related CSS to achieve the top */
function validateSticky () {
    var supportStickyValue = valiateCssValue('position'.'sticky');
    if (supportStickyValue) {
        var navBarWrap = document.querySelector('.nav-bar-wrap');
        navBarWrap.style.position = supportStickyValue;
        navBarWrap.style.top = '-24px';
        return true;
    }
    return false;
}

/** * Check whether the browser supports a CSS property value */
function valiateCssValue (key, value) {
    var prefix = ['-o-'.'-ms-'.'-moz-'.'-webkit-'.' '];
    var prefixValue = [];
    for (var i = 0; i < prefix.length; i++) {
        prefixValue.push(prefix[i] + value)
    }
    var element = document.createElement('div');
    var eleStyle = element.style;
    for (var j = 0; j < prefixValue.length; j++) {
        eleStyle[key] = prefixValue[j];
    }
    return eleStyle[key];
}
Copy the code

Here is the valiateCssValue method, which I covered in detail in another article where JS determines and tells which CSS properties (values) are supported

Then we just need to make a small adjustment in the handleScroll method and add the isSupportSticky judgment to the piece of logic that handles the top sucking

function handleScroll() {...if(! IsSupportSticky) {// This is control navigation bar suction top - suction topif((top + extraFixed) >= offsetTops.navBar) { ... } // This is control navigation bar top - cancel topif((top + extraFixed) < offsetTops.navBar) { ... }}... }Copy the code

conclusion

The article is mainly from the implementation of a practical example to expand the explanation, the basic idea, as well as the midway may encounter various problems, to consider the details. Article implementation examples of demo

I believe that we are all smart people, can draw inferences by analogy, as long as the master of essential knowledge, no matter how complex is also able to face.

Finally, it also provides a component encapsulated based on Vue implementation, from which you can feel the abstract solution architecture.

NPM address vue – scroll – nav

github vue-scroll-nav

Please do not reprint without permission