/*! Widget: stickyHeaders - updated 9/27/2017 (v2.29.0) *//* * Requires tablesorter v2.8+ and jQuery 1.4.3+ * by Rob Garrison */ ;(function ($, window) { 'use strict'; var ts = $.tablesorter || {}; $.extend(ts.css, { sticky : 'tablesorter-stickyHeader', // stickyHeader stickyVis : 'tablesorter-sticky-visible', stickyHide: 'tablesorter-sticky-hidden', stickyWrap: 'tablesorter-sticky-wrapper' }); // Add a resize event to table headers ts.addHeaderResizeEvent = function(table, disable, settings) { table = $(table)[0]; // make sure we're using a dom element if ( !table.config ) { return; } var defaults = { timer : 250 }, options = $.extend({}, defaults, settings), c = table.config, wo = c.widgetOptions, checkSizes = function( triggerEvent ) { var index, headers, $header, sizes, width, height, len = c.$headers.length; wo.resize_flag = true; headers = []; for ( index = 0; index < len; index++ ) { $header = c.$headers.eq( index ); sizes = $header.data( 'savedSizes' ) || [ 0, 0 ]; // fixes #394 width = $header[0].offsetWidth; height = $header[0].offsetHeight; if ( width !== sizes[0] || height !== sizes[1] ) { $header.data( 'savedSizes', [ width, height ] ); headers.push( $header[0] ); } } if ( headers.length && triggerEvent !== false ) { c.$table.triggerHandler( 'resize', [ headers ] ); } wo.resize_flag = false; }; clearInterval(wo.resize_timer); if (disable) { wo.resize_flag = false; return false; } checkSizes( false ); wo.resize_timer = setInterval(function() { if (wo.resize_flag) { return; } checkSizes(); }, options.timer); }; function getStickyOffset(c, wo) { var $el = isNaN(wo.stickyHeaders_offset) ? $(wo.stickyHeaders_offset) : []; return $el.length ? $el.height() || 0 : parseInt(wo.stickyHeaders_offset, 10) || 0; } // Sticky headers based on this awesome article: // http://css-tricks.com/13465-persistent-headers/ // and https://github.com/jmosbech/StickyTableHeaders by Jonas Mosbech // ************************** ts.addWidget({ id: 'stickyHeaders', priority: 54, // sticky widget must be initialized after the filter & before pager widget! options: { stickyHeaders : '', // extra class name added to the sticky header row stickyHeaders_appendTo : null, // jQuery selector or object to phycially attach the sticky headers stickyHeaders_attachTo : null, // jQuery selector or object to attach scroll listener to (overridden by xScroll & yScroll settings) stickyHeaders_xScroll : null, // jQuery selector or object to monitor horizontal scroll position (defaults: xScroll > attachTo > window) stickyHeaders_yScroll : null, // jQuery selector or object to monitor vertical scroll position (defaults: yScroll > attachTo > window) stickyHeaders_offset : 0, // number or jquery selector targeting the position:fixed element stickyHeaders_filteredToTop: true, // scroll table top into view after filtering stickyHeaders_cloneId : '-sticky', // added to table ID, if it exists stickyHeaders_addResizeEvent : true, // trigger 'resize' event on headers stickyHeaders_includeCaption : true, // if false and a caption exist, it won't be included in the sticky header stickyHeaders_zIndex : 2 // The zIndex of the stickyHeaders, allows the user to adjust this to their needs }, format: function(table, c, wo) { // filter widget doesn't initialize on an empty table. Fixes #449 if ( c.$table.hasClass('hasStickyHeaders') || ($.inArray('filter', c.widgets) >= 0 && !c.$table.hasClass('hasFilters')) ) { return; } var index, len, $t, $table = c.$table, // add position: relative to attach element, hopefully it won't cause trouble. $attach = $(wo.stickyHeaders_attachTo || wo.stickyHeaders_appendTo), namespace = c.namespace + 'stickyheaders ', // element to watch for the scroll event $yScroll = $(wo.stickyHeaders_yScroll || wo.stickyHeaders_attachTo || window), $xScroll = $(wo.stickyHeaders_xScroll || wo.stickyHeaders_attachTo || window), $thead = $table.children('thead:first'), $header = $thead.children('tr').not('.sticky-false').children(), $tfoot = $table.children('tfoot'), stickyOffset = getStickyOffset(c, wo), // is this table nested? If so, find parent sticky header wrapper (div, not table) $nestedSticky = $table.parent().closest('.' + ts.css.table).hasClass('hasStickyHeaders') ? $table.parent().closest('table.tablesorter')[0].config.widgetOptions.$sticky.parent() : [], nestedStickyTop = $nestedSticky.length ? $nestedSticky.height() : 0, // clone table, then wrap to make sticky header $stickyTable = wo.$sticky = $table.clone() .addClass('containsStickyHeaders ' + ts.css.sticky + ' ' + wo.stickyHeaders + ' ' + c.namespace.slice(1) + '_extra_table' ) .wrap('
'), $stickyWrap = $stickyTable.parent() .addClass(ts.css.stickyHide) .css({ position : $attach.length ? 'absolute' : 'fixed', padding : parseInt( $stickyTable.parent().parent().css('padding-left'), 10 ), top : stickyOffset + nestedStickyTop, left : 0, visibility : 'hidden', zIndex : wo.stickyHeaders_zIndex || 2 }), $stickyThead = $stickyTable.children('thead:first'), $stickyCells, laststate = '', setWidth = function($orig, $clone){ var index, width, border, $cell, $this, $cells = $orig.filter(':visible'), len = $cells.length; for ( index = 0; index < len; index++ ) { $cell = $clone.filter(':visible').eq(index); $this = $cells.eq(index); // code from https://github.com/jmosbech/StickyTableHeaders if ($this.css('box-sizing') === 'border-box') { width = $this.outerWidth(); } else { if ($cell.css('border-collapse') === 'collapse') { if (window.getComputedStyle) { width = parseFloat( window.getComputedStyle($this[0], null).width ); } else { // ie8 only border = parseFloat( $this.css('border-width') ); width = $this.outerWidth() - parseFloat( $this.css('padding-left') ) - parseFloat( $this.css('padding-right') ) - border; } } else { width = $this.width(); } } $cell.css({ 'width': width, 'min-width': width, 'max-width': width }); } }, getLeftPosition = function(yWindow) { if (yWindow === false && $nestedSticky.length) { return $table.position().left; } return $attach.length ? parseInt($attach.css('padding-left'), 10) || 0 : $table.offset().left - parseInt($table.css('margin-left'), 10) - $(window).scrollLeft(); }, resizeHeader = function() { $stickyWrap.css({ left : getLeftPosition(), width: $table.outerWidth() }); setWidth( $table, $stickyTable ); setWidth( $header, $stickyCells ); }, scrollSticky = function( resizing ) { if (!$table.is(':visible')) { return; } // fixes #278 // Detect nested tables - fixes #724 nestedStickyTop = $nestedSticky.length ? $nestedSticky.offset().top - $yScroll.scrollTop() + $nestedSticky.height() : 0; var tmp, offset = $table.offset(), stickyOffset = getStickyOffset(c, wo), yWindow = $.isWindow( $yScroll[0] ), // $.isWindow needs jQuery 1.4.3 yScroll = yWindow ? $yScroll.scrollTop() : // use parent sticky position if nested AND inside of a scrollable element - see #1512 $nestedSticky.length ? parseInt($nestedSticky[0].style.top, 10) : $yScroll.offset().top, attachTop = $attach.length ? yScroll : $yScroll.scrollTop(), captionHeight = wo.stickyHeaders_includeCaption ? 0 : $table.children( 'caption' ).height() || 0, scrollTop = attachTop + stickyOffset + nestedStickyTop - captionHeight, tableHeight = $table.height() - ($stickyWrap.height() + ($tfoot.height() || 0)) - captionHeight, isVisible = ( scrollTop > offset.top ) && ( scrollTop < offset.top + tableHeight ) ? 'visible' : 'hidden', state = isVisible === 'visible' ? ts.css.stickyVis : ts.css.stickyHide, needsUpdating = !$stickyWrap.hasClass( state ), cssSettings = { visibility : isVisible }; if ($attach.length) { // attached sticky headers always need updating needsUpdating = true; cssSettings.top = yWindow ? scrollTop - $attach.offset().top : $attach.scrollTop(); } // adjust when scrolling horizontally - fixes issue #143 tmp = getLeftPosition(yWindow); if (tmp !== parseInt($stickyWrap.css('left'), 10)) { needsUpdating = true; cssSettings.left = tmp; } cssSettings.top = ( cssSettings.top || 0 ) + // If nested AND inside of a scrollable element, only add parent sticky height (!yWindow && $nestedSticky.length ? $nestedSticky.height() : stickyOffset + nestedStickyTop); if (needsUpdating) { $stickyWrap .removeClass( ts.css.stickyVis + ' ' + ts.css.stickyHide ) .addClass( state ) .css(cssSettings); } if (isVisible !== laststate || resizing) { // make sure the column widths match resizeHeader(); laststate = isVisible; } }; // only add a position relative if a position isn't already defined if ($attach.length && !$attach.css('position')) { $attach.css('position', 'relative'); } // fix clone ID, if it exists - fixes #271 if ($stickyTable.attr('id')) { $stickyTable[0].id += wo.stickyHeaders_cloneId; } // clear out cloned table, except for sticky header // include caption & filter row (fixes #126 & #249) - don't remove cells to get correct cell indexing $stickyTable.find('> thead:gt(0), tr.sticky-false').hide(); $stickyTable.find('> tbody, > tfoot').remove(); $stickyTable.find('caption').toggle(wo.stickyHeaders_includeCaption); // issue #172 - find td/th in sticky header $stickyCells = $stickyThead.children().children(); $stickyTable.css({ height:0, width:0, margin: 0 }); // remove resizable block $stickyCells.find('.' + ts.css.resizer).remove(); // update sticky header class names to match real header after sorting $table .addClass('hasStickyHeaders') .bind('pagerComplete' + namespace, function() { resizeHeader(); }); ts.bindEvents(table, $stickyThead.children().children('.' + ts.css.header)); if (wo.stickyHeaders_appendTo) { $(wo.stickyHeaders_appendTo).append( $stickyWrap ); } else { // add stickyheaders AFTER the table. If the table is selected by ID, the original one (first) will be returned. $table.after( $stickyWrap ); } // onRenderHeader is defined, we need to do something about it (fixes #641) if (c.onRenderHeader) { $t = $stickyThead.children('tr').children(); len = $t.length; for ( index = 0; index < len; index++ ) { // send second parameter c.onRenderHeader.apply( $t.eq( index ), [ index, c, $stickyTable ] ); } } // make it sticky! $xScroll.add($yScroll) .unbind( ('scroll resize '.split(' ').join( namespace )).replace(/\s+/g, ' ') ) .bind('scroll resize '.split(' ').join( namespace ), function( event ) { scrollSticky( event.type === 'resize' ); }); c.$table .unbind('stickyHeadersUpdate' + namespace) .bind('stickyHeadersUpdate' + namespace, function(){ scrollSticky( true ); }); if (wo.stickyHeaders_addResizeEvent) { ts.addHeaderResizeEvent(table); } // look for filter widget if ($table.hasClass('hasFilters') && wo.filter_columnFilters) { // scroll table into view after filtering, if sticky header is active - #482 $table.bind('filterEnd' + namespace, function() { // $(':focus') needs jQuery 1.6+ var $td = $(document.activeElement).closest('td'), column = $td.parent().children().index($td); // only scroll if sticky header is active if ($stickyWrap.hasClass(ts.css.stickyVis) && wo.stickyHeaders_filteredToTop) { // scroll to original table (not sticky clone) window.scrollTo(0, $table.position().top); // give same input/select focus; check if c.$filters exists; fixes #594 if (column >= 0 && c.$filters) { c.$filters.eq(column).find('a, select, input').filter(':visible').focus(); } } }); ts.filter.bindSearch( $table, $stickyCells.find('.' + ts.css.filter) ); // support hideFilters if (wo.filter_hideFilters) { ts.filter.hideFilters(c, $stickyTable); } } // resize table (Firefox) if (wo.stickyHeaders_addResizeEvent) { $table.bind('resize' + c.namespace + 'stickyheaders', function() { resizeHeader(); }); } // make sure sticky is visible if page is partially scrolled scrollSticky( true ); $table.triggerHandler('stickyHeadersInit'); }, remove: function(table, c, wo) { var namespace = c.namespace + 'stickyheaders '; c.$table .removeClass('hasStickyHeaders') .unbind( ('pagerComplete resize filterEnd stickyHeadersUpdate '.split(' ').join(namespace)).replace(/\s+/g, ' ') ) .next('.' + ts.css.stickyWrap).remove(); if (wo.$sticky && wo.$sticky.length) { wo.$sticky.remove(); } // remove cloned table $(window) .add(wo.stickyHeaders_xScroll) .add(wo.stickyHeaders_yScroll) .add(wo.stickyHeaders_attachTo) .unbind( ('scroll resize '.split(' ').join(namespace)).replace(/\s+/g, ' ') ); ts.addHeaderResizeEvent(table, true); } }); })(jQuery, window);