sticky header - fluttering/jumping header in IE-11

时间:2018-04-18 17:48:15

标签: javascript jquery css internet-explorer-11 sticky

I have a dynamically generated custom layout with nested tables/div elements. One of the inner table implements sticky header.Right below the sticky header table is the column data which is part of the parent table. So it means, the sticky header has its own table. (Please see the attached screenshot below )

But as soon I apply the position:fixed attribute, the width goes out of align and extends the page width and not the enclosed parent width. I tried using width:inherit on parent and child(sticky-header) but to no use. In addition, if the content is too long, there will be a scroll bar as well that should ideally scroll both header and data. But since the sticky header is in it's own table, there appears a separate scroll bar just for the header.

It's complicated and I am trying to contain the width of the header within the parent width and control the scroll function (both header and data) using single scrollbar. I tried using pure CSS and JS and experienced the above issues due to the complicated nested layout.

On further investigation, I noticed the existing code is using an old jQuery plugin from 2012 (jquery.stickOnScroll.js)to handle this sticky functionality. It works in Chrome but not in IE. In IE, when the page is scrolled down, the header jumps first before coming into place. Seems like it's repainting the header layout on each scroll click.

Function call:

function sticky() {
    return jQuery("#sticky-header").stickOnScroll({
        topOffset:          0,
        bottomOffset:       0,
        viewport:           jQuery("#content"),
        setParentOnStick:   true,
        setWidthOnStick:    true
    });
}

JS -stickOnScroll:

;(function(factory) {
if (typeof define === 'function' && define.amd) {
    define(['jquery'], factory);
} else {
    factory(jQuery);
}
}(function($){

"use strict";
/*jslint plusplus: true */
/*global $,jQuery */

var
    // Check if we're working in IE. Will control the animation
    // on the fixed elements.
    isIE = ($.support.optSelected === false ? true : false),

    // List of viewports and associated array of bound sticky elements
    viewports = {},

    // Local variable to hold methods
    fn = {};

/**
 * Function bound to viewport's scroll event. Loops through
 * the list of elements that needs to be sticked for the
 * given viewport.
 * "this" keyword is assumed to be the viewport.
 *
 * @param {eventObject} jQuery's event object.
 *
 * @return {Object} The viewport (this keyword)
 *
 */
function processElements(ev) {

    var elements = viewports[$(this).prop("stickOnScroll")],
        i,j;

    // Loop through all elements bound to this viewport.
    for( i=0,j=elements.length; i<j; i++ ){

        // Scope in the variables
        // We call this anonymous funnction with the
        // current array element ( elements[i] )
        (function(o){

            var scrollTop,
                maxTop,
                cssPosition,
                footerTop,
                eleHeight,
                yAxis;

            // get this viewport options
            o = elements[i];

            // FIXME: Should the clean up of reference to removed element store the position in the array and delete it later?

            // If element has no parent, then it must have been removed from DOM...
            // Remove reference to it
            if (o !== null) {

                // jquery contains works on dom object; not jquery selections
                if (!$.contains(document.documentElement, o.ele[0])) {

                    elements[i] = o = null;

                }

            }
            if (o !== null) {

                // Get the scroll top position on the view port
                scrollTop   = o.viewport.scrollTop();

                // set the maxTop before we stick the element
                // to be it's "normal" topPosition minus offset
                maxTop      = o.getEleMaxTop();

                // TODO: What about calculating top values with margin's set?
                // pt.params.footer.css('marginTop').replace(/auto/, 0)

                // If not using the window object, then stop any IE animation
                if (o.isWindow === false && isIE) {
                    o.ele.stop();
                }

                // If the current scrollTop position is greater
                // than our maxTop value, then make element stick on the page.
                if (scrollTop >= maxTop){

                    cssPosition = {
                        position:   "fixed",
                        top:        ( o.topOffset - o.eleTopMargin )
                    };

                    if (o.isWindow === false) {

                        cssPosition = {
                            position:   "absolute",
                            top:        ( ( scrollTop + o.topOffset ) -  o.eleTopMargin )
                        };

                    }

                    o.isStick = true;

                    // ---> HAS FOOTER ELEMENT?
                    // check to see if it we're reaching the footer element,
                    // and if so, scroll the item up with the page
                    if  (o.footerElement.length) {

                        // Calculate the distance from the *bottom* of the fixed
                        // element to the footer element, taking into consideration
                        // the bottomOffset that may have been set by the user.
                        footerTop   = o.getEleTopPosition(o.footerElement);
                        eleHeight   = o.ele.outerHeight();
                        yAxis       = ( cssPosition.top + eleHeight +
                                        o.bottomOffset + o.topOffset );

                        if (o.isWindow === false) {

                            yAxis = (eleHeight + o.bottomOffset + o.topOffset);

                        } else {

                            yAxis = ( cssPosition.top + scrollTop +
                                      eleHeight + o.bottomOffset );

                            footerTop = o.getElementDistanceFromViewport(o.footerElement);

                        }

                        // If the footer element is overstopping the sticky element
                        // position, then adjust it so that we make room for the
                        // fotoer element.
                        if (yAxis > footerTop) {

                            if (o.isWindow === true) {

                                cssPosition.top = (
                                        footerTop - ( scrollTop + eleHeight + o.bottomOffset )
                                    );

                            // Absolute positioned element
                            } else {

                                cssPosition.top = (scrollTop - (yAxis - footerTop));

                            }

                        }

                    }

                    // If o.setParentOnStick is true, then set the
                    // height to this node's parent.
                    if (o.setParentOnStick === true) {

                        o.eleParent.css("height", o.eleParent.height());

                    }

                    // If o.setWidthOnStick is true, then set the width on the
                    // element that is about to be Sticky.
                    if (o.setWidthOnStick === true) {

                        o.ele.css("width", o.ele.css("width"));

                    }

                    // If we have additional stick offset, apply it now
                    if (!o.isViewportOffsetParent) {

                        cssPosition.top = (
                            cssPosition.top - o.getElementDistanceFromViewport(o.eleOffsetParent)
                        );

                    }

                    // Stick the element
                    if (isIE && o.isWindow === false) {

                        o.ele
                            .addClass(o.stickClass)
                            .css("position", cssPosition.position)
                            .animate({top: cssPosition.top}, 150);

                    } else {

                        o.ele
                            .css(cssPosition)
                            .addClass(o.stickClass);

                    }

                    // If making element stick now, then trigger
                    // onStick callback if any
                    if (o.wasStickCalled === false) {

                        o.wasStickCalled = true;

                        setTimeout(function(){

                            if (o.isOnStickSet === true) {

                                o.onStick.call(o.ele, o.ele);

                            }

                            o.ele.trigger("stickOnScroll:onStick", [o.ele]);

                        }, 20);

                    }

                // ELSE, If the scrollTop of the view port is
                // less than the maxTop, then throw the element back into the
                // page normal flow
                } else if (scrollTop <= maxTop) {

                    if (o.isStick) {

                        // reset element
                        o.ele
                            .css({
                                position: "",
                                top: ""
                            })
                            .removeClass(o.stickClass);

                        o.isStick = false;

                        // Reset parent if o.setParentOnStick is true
                        if (o.setParentOnStick === true) {

                            o.eleParent.css("height", "");

                        }

                        // Reset the element's width if o.setWidthOnStick is true
                        if (o.setWidthOnStick === true) {

                            o.ele.css("width", "");

                        }

                        o.wasStickCalled = false;

                        setTimeout(function(){

                            // Execute the onUnStick if defined
                            if (o.isOnUnStickSet) {

                                o.onUnStick.call( o.ele, o.ele );

                            }

                            o.ele.trigger("stickOnScroll:onUnStick", [o.ele]);

                        }, 20);

                    }
                }

                // Recalculate the original top position of the element...
                // this could have changed from when element was initialized
                // - ex. elements were inserted into DOM. We re-calculate only
                // if the we're at the very top of the viewport, so that we can
                // get a good position.
                if (scrollTop === 0) {

                    o.setEleTop();

                }

            }// is element setup null?

        })( elements[i] );

    }//end: for()

    return this;

}//end: processElements()


/**
 * Make the selected items stick to the top of the viewport
 * upon reaching a scrolling offset.
 * This method manipulates the following css properties on
 * the element that is to be sticky: top, position.
 * Elements also receive a css class named 'hasStickOnScroll'.
 *
 * @param {Object} options
 * @param {Integer} [options.topOffset=0]
 * @param {Integer} [options.bottomOffset=5]
 * @param {Object|HTMLElement|jQuery} [options.footerElement=null]
 * @param {Object|HTMLElement|jQuery} [options.viewport=window]
 * @param {String} [options.stickClass="stickOnScroll-on"]
 * @param {Boolean} [options.setParentOnStick=false]
 * @param {Boolean} [options.setWidthOnStick=false]
 * @param {Function} [options.onStick=null]
 * @param {Function} [options.onUnStick=null]
 *
 * @return {jQuery} this
 *
 */
$.fn.stickOnScroll = function(options) {
    return this.each(function(){

        // If element already has stickonscroll, exit.
        if ($(this).hasClass("hasStickOnScroll")) {
            return this;
        }

        // Setup options for tis instance
        var o   = $.extend({}, {
                        topOffset:          0,
                        bottomOffset:       5,
                        footerElement:      null,
                        viewport:           window,
                        stickClass:         'stickOnScroll-on',
                        setParentOnStick:   false,
                        setWidthOnStick:    false,
                        onStick:            null,
                        onUnStick:          null
                    }, options),
            viewportKey,
            setIntID,
            setIntTries = 1800; // 1800 tries * 100 milliseconds = 3 minutes

        o.isStick                   = false;
        o.ele                       = $(this).addClass("hasStickOnScroll");
        o.eleParent                 = o.ele.parent();
        o.eleOffsetParent           = o.ele.offsetParent();
        o.viewport                  = $(o.viewport);
        o.eleTop                    = 0;
        o.eleTopMargin              = parseFloat(
                                        (o.ele.css("margin-top") || 0)
                                    ) || 0;
        o.footerElement             = $(o.footerElement);
        o.isWindow                  = true;
        o.isOnStickSet              = $.isFunction(o.onStick);
        o.isOnUnStickSet            = $.isFunction(o.onUnStick);
        o.wasStickCalled            = false;
        o.isViewportOffsetParent    = true;

        /**
         * Retrieves the element's top position based on the type of viewport
         * and sets on the options object for the instance. This Top position
         * is the element top position relative to the the viewport.
         *
         * @return {Integer}
         */
        o.setEleTop = function(){

            if (o.isStick === false) {

                if (o.isWindow) {

                    o.eleTop = o.ele.offset().top;

                } else {

                    o.eleTop = ( o.ele.offset().top - o.viewport.offset().top );

                }

            }

        }; //end: o.setEleTop()

        /**
         * REturns an elements top position in relation
         * to the viewport's Top Position.
         *
         * @param {jQuery} $ele
         *          This element must be inside the viewport
         *
         * @return {Integer}
         *
         */
        o.getEleTopPosition = function($ele) {

            var pos = 0;

            if (o.isWindow) {

                pos = $ele.offset().top;

            } else {

                pos = ( $ele.offset().top - o.viewport.offset().top );

            }

            return pos;

        }; /* o.getEleTopPosition() */

        /**
         * Get's the MAX top position for the element before it
         * is made sticky. In some cases the max could be less
         * than the original position of the element, which means
         * the element would always be sticky... in these instances
         * the max top will be set to the element's top position.
         *
         * @return {Integer}
         */
        o.getEleMaxTop = function() {

            var max = ( ( o.eleTop - o.topOffset ));

            if (!o.isWindow) {

                max = (max + o.eleTopMargin);

                // If ele parent is not the viewport, then adjust the max top


            }

            return max;

        }; //end: o.getEleMaxTop()

        /**
         * Gets the distance between the top of the element and the
         * top of the viewport. Basically the offset from the top of
         * the "page" inside the viewport. This distance is alwasy the
         * same even if the viewport is scrolled. The only time it
         * changes is when elements are inserted or removed above the
         * the Element or item above it are hidden/displayed.
         * Methods uses the Position() values until it reaches the
         * viewport
         */
        o.getElementDistanceFromViewport = function($ele) {

            var distance    = $ele.position().top,
                $parent     = $ele.offsetParent();

            // If the parent element is the root body element, then
            // we've reached the last possible offsetParent(). Exit
            if ($parent.is("body") || $parent.is("html")) {

                return distance;

            }

            // If the positioned parent of this element is NOT
            // the viewport, then add the distance of that element's
            // top position
            if ($parent[0] !== o.viewport[0] ) {

                distance = (
                    distance +
                    o.getElementDistanceFromViewport( $parent )
                );

            // ELSE, this is the viewport... Adjust the elements
            // Distance by adding on the amount of scroll the element
            // currently has
            } else {

                distance = (distance + o.viewport.scrollTop());

            }

            return distance;

        }; /* end: .getElementDistanceFromViewport() */

        // If setParentOnStick is true, and the parent element
        // is the <body>, then set setParentOnStick to false.
        if (o.setParentOnStick === true && o.eleParent.is("body")){

            o.setParentOnStick = false;

        }

        if (!$.isWindow(o.viewport[0])) {

            o.isWindow  = false;

        }

        /**
         * Adds this sticky element to the list of element for the viewport.
         *
         */
        function addThisEleToViewportList() {

            o.setEleTop();

            viewportKey = o.viewport.prop("stickOnScroll");

            // If the viewport is not the Window element, and the view port is not the
            // stick element's imediate offset parent, then we need to adjust the
            // top-offset so that element are position correctly.
            // See issue #3 on github
            if (!o.isWindow) {

                o.isViewportOffsetParent    = ( o.eleOffsetParent[0] === o.viewport[0] );

            }

            // If this viewport is not yet defined, set it up now
            if (!viewportKey) {

                viewportKey = "stickOnScroll" + String(Math.random()).replace(/\D/g,"");
                o.viewport.prop("stickOnScroll", viewportKey);
                viewports[viewportKey] = [];
                o.viewport.on("scroll", processElements);

            }

            // Push this element's data to this view port's array
            viewports[viewportKey].push(o);

            // Trigger a scroll even
            o.viewport.scroll();

        } /* end: addThisEleToViewportList() */

        // If Element is not visible, then we have to wait until it is
        // in order to set it up. We need to obtain the top position of
        // the element in order to make the right decision when it comes
        // to making the element sticky.
        if (o.ele.is(":visible")) {

            addThisEleToViewportList();

        } else {

            setIntID = setInterval(function(){

                if (o.ele.is(":visible") || !setIntTries) {

                    clearInterval(setIntID);
                    addThisEleToViewportList();

                }

                --setIntTries;

            }, 100);

        }

        return this;

    });//end: each()

};//end: $.fn.stickOnScroll()

}));

On Chrome debugger, I noticed the following behavior happening using the stickOnscroll plugin.

The stickyheader table is initially placed inside a parent table <td>. On scroll down, the td height is assigned. The sticky header table width is set to parent table width. Position set to absolute. Most importantly top attribute is dynamically calculated.

Before scroll:

<td colspan="21" style="">
<div id="sticky-header" class="hasStickOnScroll" style="">
<table width="1424" cellspacing="0" cellpadding="0" border="1&quot;1&quot;">    

After scroll:

<td colspan="21" style="height: 317px;">
<div id="sticky-header" class="hasStickOnScroll stickOnScroll-on"  style="width: 1967px; position: absolute; top: 280.667px;">
<table width="1424" cellspacing="0" cellpadding="0" border="1&quot;1&quot;"> 

I believe this top: 280.667px is what causing the jumping/flickering behavior in IE-11 while it's transitioned smoothly in Chrome.

Any tips to fix this issue? On further investigation, this is what the plugin does.

 if (scrollTop >= maxTop){                      


                    cssPosition = {
                        position:   "fixed",
                        top:        ( o.topOffset - o.eleTopMargin )
                    };

                    if (o.isWindow === false) {



                        cssPosition = {
                            position:   "absolute",
                            top:        ( ( scrollTop + o.topOffset ) -  o.eleTopMargin )

                        };

                    }

if (o.isWindow === false) is returned true and hence the position is set as absolute and top is dynamically calculated. Position fixed attribute will break the sticky header width(inherits the window width instead) and will become non scrollable.

Jumping Header Demo - jsfiddle

This code runs like a snail in jsfiddle opened in IE-11. :(

0 个答案:

没有答案