StickyTableHeaders icon indicating copy to clipboard operation
StickyTableHeaders copied to clipboard

header overflow if the table is inside a container

Open ghost opened this issue 12 years ago • 24 comments

I like what you have done for making a fixed header html table. But since you are using 'position: fixed', there is this drawback of headers overflowing the container boundary, if the table is inside a containing div which has a given width/height and handles overflow using scrollbars.

I have modified your fiddle to show this here:

http://jsfiddle.net/cGF4W/

Try scrolling using the outside scrollbar & you'll see headers overflowing the boundary. I think you can get rid of this using 'position:absolute' but then the scrolling logic goes for a toss.

I think the scenario of having tables within a container is very generic and when handled, will make this plugin very versatile.

Thanks.

ghost avatar Dec 19 '11 09:12 ghost

Thanks for the feedback. I actually designed the plugin to avoid having to nest my long tables inside fixed size containers, but I get your point.

I'll put it on my list of things that should be handled when I get the time to look at the plugin again.

Cheers Jonas

jmosbech avatar Dec 19 '11 21:12 jmosbech

Jonas,

+1 on MrHunt's suggestion.

Merg

merg212 avatar Jan 06 '12 14:01 merg212

If anyone is interested, I modified this today to get this working: http://jsfiddle.net/LqZ2T/15/

Seems to work OK in IE 8, Firefox, and Chrome. I tried to comment this the best I could. Unfortunately this was not easy, so there were a bunch of changes. I will try to go through them here quickly:

1 - First - if your table is inside a div that handles overflow, pass it into options:

$("table").stickyTableHeaders({container: "#container"});

2 - Introduce base.$container.

If the user passed in a container element, base.$container points to that, otherwise it points to base.$window. All of the operations are rewired to work against base.$container instead of base.$window:

base.$container = base.options.container != null ? $(base.options.container) : base.$window;

Events now get wired to base.$container instead of base.$window:

base.$container.scroll(base.toggleHeaders);
base.$container.resize(base.toggleHeaders);
base.$container.resize(base.updateWidth);

3 - Now the trick is in how we calculate how far you need to scroll to activate and deactivate the plugin. Obviously we need to grab the distance from the top of the viewport to the top of the table.You would think you can just use .position for this, but the user might have their table inside a position: "static" element (I point to stackoverflow thread in the jsfiddle comments).

So to do this reliably, we use offset() to grab the top of the viewport and the top of the table. We simply take the difference to see how much we can scroll before an activation is needed. Also - add in support for captions.

Note: base.getContainerOffset() below simply returns the correct offset depending on if the user passed in a container, or if the container is actually the window.

var startTopOffset = base.$el.offset().top - base.getContainerOffset().top;
var caption = base.$el.find('caption');
if (caption.length){
    startTopOffset += caption.height();
}
base.scrollAmountToActivate = startTopOffset;
base.scrollAmountToDeactivate = base.scrollAmountToActivate + base.$el.height();

4 - Modify the toggleHeaders function: - Use base.getContainerOffset function to grab the correct offset. This one is important since it is used to set the "top" css property on the cloned header. - Use base.$container to get the scrollTop / scrollLeft

base.toggleHeaders = function () {
    base.$el.each(function () {
    var $this = $(this);
    var newTopOffset = isNaN(base.options.fixedOffset) ?
        base.options.fixedOffset.height() : base.options.fixedOffset;
    var offset = base.getContainerOffset();
    var scrollTop = base.$container.scrollTop() + newTopOffset;
    var scrollLeft = base.$container.scrollLeft();
    if ((scrollTop > base.scrollAmountToActivate) && (scrollTop < base.scrollAmountToDeactivate)) {
        var newLeft = offset.left - scrollLeft;
        if (base.isCloneVisible && (newLeft === base.leftOffset) && (newTopOffset === base.topOffset)) {
            return;
        }
    base.$clonedHeader.css({
        'top': newTopOffset + offset.top,
        'margin-top': 0,
            'left' : newLeft,
        'display': 'block'
        });

... snip ...

5 - And now for the last bit. If your table is inside of a div with horizontal scrollbars, it is possible that the table is wider than the div itself. Since the cloned table header is displayed using "fixed" positioning, it is outside the normal document flow. This means that when the plugin activates, the cloned header would show "on top of" its parent container. What we have to do is to clip the cloned header after it activates to the width of its containing parent. In order to do this correctly, we have to know how wide the scrollbar is. I stole the "getScrollbarWidth" function from stackoverflow, and then calculate base.parentClientWidth on startup:

base.parentClientWidth = base.$container.width() - getScrollbarWidth();

And then at the end of updateWidth we clip if necessary:

if(base.$clonedHeader.width() > base.parentClientWidth)
{
    var scrollLeft = base.$container.scrollLeft();
    var clipLeft = scrollLeft;
    var clipRight =  base.parentClientWidth + scrollLeft;
    base.$clonedHeader.css({
        'clip': 'rect(0px, ' + clipRight + 'px, ' 
                                + base.$clonedHeader.height() + 'px,' 
                                + clipLeft + 'px)'
    });
}

jpeck avatar Aug 25 '12 20:08 jpeck

Jeff - thanks for taking the time to come up with this solution. Hopefully it can serve as an inspiration to others having the same requirements.

Best Jonas

jmosbech avatar Sep 12 '12 19:09 jmosbech

Why not merge this into the codebase?

mdpatrick avatar Mar 29 '13 19:03 mdpatrick

Two reasons:

  • I didn't think that this scenario was frequent enough to justify the added complexity
  • The provided fiddle didn't seem to work and I didn't have the time to debug at that point in time

I might reconsider if somebody can come up with a working example :)

Best Jonas

jmosbech avatar Mar 29 '13 21:03 jmosbech

Ah... Yeah, I see now that if the outer container (body) is scrolled, then the table heading does some strange things.

mdpatrick avatar Mar 29 '13 21:03 mdpatrick

Jonas / mdpatrick - Can you give me any more details about what wasn't working for you in the provided fiddle? I'm interested in getting this to work flawlessly since I use this at work. This has worked pretty well for me in the scenarios I use it in, and I bet it would be pretty simple to fix any nagging issues.

One thing I did notice just now is that resizing the browser window throws off the initial calculations that are made when the plugin initializes. Is there anything else you guys noticed? Can you give me exact steps to reproduce? I'm super busy with work right now, but I'd be willing to throw some spare time at this if you're interested.

Thanks, Jeff Peck

jpeck avatar Mar 30 '13 01:03 jpeck

Can you guys try this out and report back any issues? This seems to be working for me. The code isn't very clean, so please consider this a proof of concept for now.

http://jsfiddle.net/LqZ2T/42/

Here is a recap of what I did:

1 - The tricky part of this is calculating the position of the header after an activation. I moved this code into a function:

        base.calcNewHeaderPosition = function() {
            var windowScrollTop = base.$window.scrollTop();
            var containerOffset = base.getContainerOffset();
            var elementOffset = base.$el.offset();
            var scrollLeft = base.$container.scrollLeft() + base.$window.scrollLeft();
            var newLeft = containerOffset.left - scrollLeft;
            var newTop;

            /* 
             * If we're activated and the user has scrolled the window enough to 
             * where the top of the container div is outside the viewport - peg the 
             * header to the top of the viewport.
             */
            if(windowScrollTop > containerOffset.top) {
                newTop = 0;
            }
            else {
               /* Otherwise peg to the top of the actual container. */
                newTop = containerOffset.top - windowScrollTop;
            }
            base.$clonedHeader.css({
                'top': newTop,
                'margin-top': 0,
                'left' : newLeft,
                'display': 'block'
            });            
        };

2 - Wire up the window scroll event. This is doing 2 things. The first thing it does is call toggleHeaders(). This checks to see if we have scrolled the window or the container enough to trigger the activation. Then if the header is activated, it gets repositioned on every scroll event. This effectively lets the scroll event "override" the position of the stick header outside of the toggleHeaders function.

        base.$window.scroll(function() { 
            base.toggleHeaders();
            if(base.isCloneVisible) {
                base.calcNewHeaderPosition();
            }
        });

3 - Alter the code that checks if we need to perform the sticky header activation such that it recognizes when the window has been scrolled as well as the container.


        var scrolledEnoughToActivate = (scrollTop > base.scrollAmountToActivate) || (windowScrollTop > elementOffset.top);
        if (  scrolledEnoughToActivate && (scrollTop < base.scrollAmountToDeactivate) ) {
            var newLeft = containerOffset.left - scrollLeft;
            if (base.isCloneVisible && (newLeft === base.leftOffset) && (newTopOffset === base.topOffset)) {
                return;
            }
            //... use the calcNewHeaderPosition function to position the sticky header...
            base.calcNewHeaderPosition();
            base.updateWidth();

This was harder than it looks, and I may have missed something. Please try it out, and if you find any issues let me know! I did NOT test this code without a parent container, so I could have broken something there. We're getting closer...

jpeck avatar Mar 30 '13 04:03 jpeck

Now THIS looks like you might be on to something! Horizontal scroll doesnt break this one like it did in the previous one you posted. +1

Now I've got to decide whether to try to get this one working for my needs via your instructions, or instead try to patch this somewhat buggy and apparently abandoned (not updated in 2 years) project that also allows horizontal overflow with the sticky header for extra long tables which you can observe here: http://jsfiddle.net/D2p63/21/

Basically, the vast majority of the sticky header table solutions out there just dont handle super wide tables in a container (e.g. in bootstrap's grid system) well.. Anyway, thanks for sharing! I'll poke around with this sometime in the next 48 hours.

mdpatrick avatar Mar 31 '13 21:03 mdpatrick

Thanks guys. It looks promising. I'll dive into this as soon as possible and get back to you.

Best Jonas

jmosbech avatar Apr 01 '13 11:04 jmosbech

Hey, I ran both the original stickytableheaders and the overflow header patched version through the same beautifier and created a gist with revisions so I could see a proper diff. Might be useful to you, Jonas...

https://gist.github.com/mdpatrick/5286727/revisions

mdpatrick avatar Apr 01 '13 18:04 mdpatrick

@jpeck:

Some input...

  • container parameter could optionally accept actual jquery object
  • parentClientWidth update might be better placed inside updateWidth in case container changes sizes, clip should update.
  • Cell padding/margin seems to be an issue since when you set the thead to fixed it no longer automatically adjusts based on the tbody. I tried changing "width()" to "outerWidth(true)" on the line that harvests "$origCell"'s width and adjusts the duplicate thead's cells to match... this seems to fix it, but I only tested in chrome.

You can see the diff of some of this in the revisions here... https://gist.github.com/mdpatrick/5286727/revisions

Also here is your jsfiddle updated with some of the changes with outerWidth... http://jsfiddle.net/LqZ2T/43/

mdpatrick avatar Apr 01 '13 22:04 mdpatrick

@jmosbech - I don't know if anyone is still interested in this, but I found an additional problem and fix for the solution I provided. If the parent container happens to have any left padding, the sticky table header gets activated in the wrong position. This can be demonstrated here: http://jsfiddle.net/LqZ2T/49/

Preliminary fix is here: http://jsfiddle.net/LqZ2T/59/

base.calcNewHeaderPosition = function() {
    var windowScrollTop = base.$window.scrollTop();
    var containerOffset = base.getContainerOffset();
    var elementOffset = base.$el.offset();
    var scrollLeft = base.$container.scrollLeft() + base.$window.scrollLeft();

    // PART 1 of fix is here. Simply add in parent's left padding on activation.
    var newLeft = containerOffset.left - scrollLeft +  parseInt(base.$container.css("padding-left"));
    var newTop;                    
    if(windowScrollTop > containerOffset.top) {
        newTop = 0;
    }
    else {
       newTop = containerOffset.top - windowScrollTop;
    }
    base.$clonedHeader.css({
        'top': newTop,
        'margin-top': 0,
        'left' : newLeft,
        'display': 'block'
    });            
};

And we have to do some voodoo in our clipping code too:

if(base.$clonedHeader.width() > base.parentClientWidth)
{
    // PART 2: Compensate for padding in scrollLeft
    var paddingLeft = parseInt(base.$container.css("padding-left"));
    var scrollLeft = base.$container.scrollLeft() - paddingLeft;
    var clipLeft = scrollLeft;

    // PART 3: Compensate for padding in clip right
    var clipRight =  base.parentClientWidth + scrollLeft + paddingLeft;
    base.$clonedHeader.css({
        'clip': 'rect(0px, ' + clipRight + 'px, ' 
                                + base.$clonedHeader.height() + 'px,' 
                                + clipLeft + 'px)'
     });
}

jpeck avatar Aug 26 '13 21:08 jpeck

Thanks for running with this @jpeck. Apparently quite a few people are using the plugin in a similar way as you so I'm keeping the issue open for now, hoping that I'll find some time to look into the solution in detail soon.

jmosbech avatar Sep 01 '13 17:09 jmosbech

@jpeck There is a problem with Bootstrap responsive table. http://jsfiddle.net/LqZ2T/82/

  • The header width is wrong.
  • On narrow size of screen when horizontal scroll, the header is not hidden. (scroll to left the right columns show up. scroll to right the left columns show up.)

ve3 avatar Apr 24 '14 23:04 ve3

Hi Jonas, it is very good script for initial purpose, but the problem with scrollable area still exist. The attached is the provided demo file (scrollable-div.htm) with couple small changes made by embedded style - just to highlight the issue - visible overflow and fixed table width. As you can see, the header with horizontal scrolling overlaps scrollable area plus position calculation is wrong. Any suggestions on fix or workaround? thead_hor_scroll_issue

SergeTiger avatar Dec 03 '14 03:12 SergeTiger

I suggest to change scrollLeft = base.$scrollableArea.scrollLeft(), to scrollLeft = base.isWindowScrolling ? base.$scrollableArea.scrollLeft() : 0, https://github.com/jmosbech/StickyTableHeaders/blob/master/js/jquery.stickytableheaders.js#L130

bodja avatar Aug 13 '15 15:08 bodja

Hacky fix that doesn't require modifying the plugin:

$('table').each(function (i, e) {
    var table = $(e);
    var container = table.parents('.table-responsive').first();
    var floating = false;

    table.stickyTableHeaders();

    if (container.length > 0) {
        var header = table.find('.tableFloatingHeaderOriginal');
        header.css('overflow-x', 'hidden');

        var headerOriginalDisplay = header.css('display');
        var headerOriginalLeft = header.css('left');

        // Fix the header after a small delay in order to not interfere with the plugin
        var timer;
        function fixHeader(condition) {
            if (floating === true && condition === true) {
                clearTimeout(timer);
                header.hide();
                timer = setTimeout(function () {
                    header.fadeIn().css('display', headerOriginalDisplay);
                    header.width(container.width());
                    header.css('left', container.scrollLeft() + headerOriginalLeft);
                    header.scrollLeft(container.scrollLeft());
                }, 100);
            }
        }

        $(window).resize(function () { fixHeader(true); });
        $(window).scroll(function () { fixHeader(hasHorizontalScrollBar(container)); });
        container.scroll(function () { header.scrollLeft(container.scrollLeft()); });

        table.on('enabledStickiness.stickyTableHeaders', function () {
            floating = true;
            fixHeader(true);
        });

        table.on('disabledStickiness.stickyTableHeaders', function () {
            floating = false;
            clearTimeout(timer);
            header.css('display', headerOriginalDisplay);
        });
    }
});

EtienneHeitz avatar Jan 21 '16 15:01 EtienneHeitz

What about this issue? This is fixed? I'm having the same problem with horizontal scroll bars.

MartinGian avatar Jul 29 '16 14:07 MartinGian

@SergeTiger @jpeck

Hey guys, I made some changes in the solution provided by jpeck (and applied the fix passed by cdfre here https://github.com/jmosbech/StickyTableHeaders/issues/68 and It worked here in my case.

Here is the new solution (stressed with data) http://jsfiddle.net/2oeuvzLz/

I basically substituted all the ".width()" to ".outerWidth()" and It worked fine.

gabazureus avatar Oct 11 '16 13:10 gabazureus

@gabazureus there is a problem when resize browser the header does not resize clips...

maxlibin avatar Feb 07 '17 03:02 maxlibin

@gabazureus I used your source code in http://jsfiddle.net/2oeuvzLz/ it works well when no browser is not resized. could you advice how to resize header when browser is resized

LinWang2 avatar May 16 '19 09:05 LinWang2

Add the below CSS to your table to fix the table header overflow of a container when scrolling to the left and right sides.

clip-path: inset(0 0 0 0);

Like the example below. In my case, I use JQuery, Bootstrap, and StickyTableHeaders.

<nav id="navbar" style="position: fixed; top: 0, right: 0; left: 0; height: 50px;">
  Navbar
</nav>

<div class="row">
  <div class="col">
    <div class="table-responsive">
      <table id="sticky" class="table table-bordered" style="clip-path: inset(0 0 0 0);"> 
        <thead>
          <tr>
            ....
          </tr>
        </thead>
        <tbody>
          <tr>
            ...
          </tr>
        </tbody>
      </table>
    </div>
  </div>
</div>

<script>
$('#sticky').stickyTableHeaders({ fixedOffset: $('#navbar'), scrollableArea: window });
</script>

ilham76c avatar Dec 22 '22 09:12 ilham76c