/** @preserve jQuery.floatThead 2.1.2 - https://mkoryak.github.io/floatThead/ - Copyright (c) 2012 - 2018 Misha Koryak **/ // @license MIT /* @author Misha Koryak * @projectDescription position:fixed on steroids. Lock a table header in place while scrolling. * * Dependencies: * jquery 1.9.0 + [required] OR jquery 1.7.0 + jquery UI core * * https://mkoryak.github.io/floatThead/ * * Tested on FF13+, Chrome 21+, IE8, IE9, IE10, IE11 */ (function( $ ) { /** * provides a default config object. You can modify this after including this script if you want to change the init defaults * @type {!Object} */ $.floatThead = $.floatThead || {}; $.floatThead.defaults = { headerCellSelector: 'tr:visible:first>*:visible', //thead cells are this. zIndex: 1001, //zindex of the floating thead (actually a container div) position: 'auto', // 'fixed', 'absolute', 'auto'. auto picks the best for your table scrolling type. top: 0, //String or function($table) - offset from top of window where the header should not pass above bottom: 0, //String or function($table) - offset from the bottom of the table where the header should stop scrolling scrollContainer: function($table) { // or boolean 'true' (use offsetParent) | function -> if the table has horizontal scroll bars then this is the container that has overflow:auto and causes those scroll bars return $([]); }, responsiveContainer: function($table) { // only valid if scrollContainer is not used (ie window scrolling). this is the container which will control y scrolling at some mobile breakpoints return $([]); }, getSizingRow: function($table, $cols, $fthCells){ // this is only called when using IE, // override it if the first row of the table is going to contain colgroups (any cell spans greater than one col) // it should return a jquery object containing a wrapped set of table cells comprising a row that contains no col spans and is visible return $table.find('tbody tr:visible:first>*:visible'); }, floatTableClass: 'floatThead-table', floatWrapperClass: 'floatThead-wrapper', floatContainerClass: 'floatThead-container', copyTableClass: true, //copy 'class' attribute from table into the floated table so that the styles match. autoReflow: false, //(undocumented) - use MutationObserver api to reflow automatically when internal table DOM changes debug: false, //print possible issues (that don't prevent script loading) to console, if console exists. support: { //should we bind events that expect these frameworks to be present and/or check for them? bootstrap: true, datatables: true, jqueryUI: true, perfectScrollbar: true }, floatContainerCss: {"overflow-x": "hidden"} // undocumented - css applied to the floatContainer }; var util = (function underscoreShim(){ var that = {}; var hasOwnProperty = Object.prototype.hasOwnProperty, isThings = ['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp']; that.has = function(obj, key) { return hasOwnProperty.call(obj, key); }; that.keys = Object.keys || function(obj) { if (obj !== Object(obj)) throw new TypeError('Invalid object'); var keys = []; for (var key in obj) if (that.has(obj, key)) keys.push(key); return keys; }; var idCounter = 0; that.uniqueId = function(prefix) { var id = ++idCounter + ''; return prefix ? prefix + id : id; }; $.each(isThings, function(){ var name = this; that['is' + name] = function(obj) { return Object.prototype.toString.call(obj) == '[object ' + name + ']'; }; }); that.debounce = function(func, wait, immediate) { var timeout, args, context, timestamp, result; return function() { context = this; args = arguments; timestamp = new Date(); var later = function() { var last = (new Date()) - timestamp; if (last < wait) { timeout = setTimeout(later, wait - last); } else { timeout = null; if (!immediate) result = func.apply(context, args); } }; var callNow = immediate && !timeout; if (!timeout) { timeout = setTimeout(later, wait); } if (callNow) result = func.apply(context, args); return result; }; }; return that; })(); var canObserveMutations = typeof MutationObserver !== 'undefined'; //browser stuff var ieVersion = function(){for(var a=3,b=document.createElement("b"),c=b.all||[];a = 1+a,b.innerHTML="",c[0];);return 4').css('width', 0).append( $('').css('max-width', '100%').append( $('').append( $('elements, but webkit cannot var $window = $(window); var buggyMatchMedia = isFF && window.matchMedia; // TODO remove when fixed: https://bugzilla.mozilla.org/show_bug.cgi?id=774398 if(!window.matchMedia || buggyMatchMedia) { var _beforePrint = window.onbeforeprint; var _afterPrint = window.onafterprint; window.onbeforeprint = function () { _beforePrint && _beforePrint(); $window.triggerHandler("beforeprint"); }; window.onafterprint = function () { _afterPrint && _afterPrint(); $window.triggerHandler("afterprint"); }; } /** * @param eventName * @param cb */ function windowResize(eventName, cb){ if(ieVersion == 8){ //ie8 is crap: https://github.com/mkoryak/floatThead/issues/65 var winWidth = $window.width(); var debouncedCb = util.debounce(function(){ var winWidthNew = $window.width(); if(winWidth != winWidthNew){ winWidth = winWidthNew; cb(); } }, 1); $window.on(eventName, debouncedCb); } else { $window.on(eventName, util.debounce(cb, 1)); } } function getClosestScrollContainer($elem) { var elem = $elem[0]; var parent = elem.parentElement; do { var pos = window .getComputedStyle(parent) .getPropertyValue('overflow'); if (pos != 'visible') break; } while (parent = parent.parentElement); if(parent == document.body){ return $([]); } return $(parent); } function debug(str){ window && window.console && window.console.error && window.console.error("jQuery.floatThead: " + str); } //returns fractional pixel widths function getOffsetWidth(el) { var rect = el.getBoundingClientRect(); return rect.width || rect.right - rect.left; } /** * try to calculate the scrollbar width for your browser/os * @return {Number} */ function scrollbarWidth() { var d = document.createElement("scrolltester"); d.style.cssText = 'width:100px;height:100px;overflow:scroll!important;position:absolute;top:-9999px;display:block'; document.body.appendChild(d); var result = d.offsetWidth - d.clientWidth; document.body.removeChild(d); return result; } /** * Check if a given table has been datatableized (https://datatables.net) * @param $table * @return {Boolean} */ function isDatatable($table){ if($table.dataTableSettings){ for(var i = 0; i < $table.dataTableSettings.length; i++){ var table = $table.dataTableSettings[i].nTable; if($table[0] == table){ return true; } } } return false; } function tableWidth($table, $fthCells, isOuter){ // see: https://github.com/mkoryak/floatThead/issues/108 var fn = isOuter ? "outerWidth": "width"; if(isTableWidthBug && $table.css("max-width")){ var w = 0; if(isOuter) { w += parseInt($table.css("borderLeft"), 10); w += parseInt($table.css("borderRight"), 10); } for(var i=0; i < $fthCells.length; i++){ w += getOffsetWidth($fthCells.get(i)); } return w; } else { return $table[fn](); } } $.fn.floatThead = function(map){ map = map || {}; if(ieVersion < 8){ return this; //no more crappy browser support. } var mObs = null; //mutation observer lives in here if we can use it / make it if(util.isFunction(isTableWidthBug)) { isTableWidthBug = isTableWidthBug(); } if(util.isString(map)){ var command = map; var args = Array.prototype.slice.call(arguments, 1); var ret = this; this.filter('table').each(function(){ var $this = $(this); var opts = $this.data('floatThead-lazy'); if(opts){ $this.floatThead(opts); } var obj = $this.data('floatThead-attached'); if(obj && util.isFunction(obj[command])){ var r = obj[command].apply(this, args); if(r !== undefined){ ret = r; } } }); return ret; } var opts = $.extend({}, $.floatThead.defaults || {}, map); $.each(map, function(key, val){ if((!(key in $.floatThead.defaults)) && opts.debug){ debug("Used ["+key+"] key to init plugin, but that param is not an option for the plugin. Valid options are: "+ (util.keys($.floatThead.defaults)).join(', ')); } }); if(opts.debug){ var v = $.fn.jquery.split("."); if(parseInt(v[0], 10) == 1 && parseInt(v[1], 10) <= 7){ debug("jQuery version "+$.fn.jquery+" detected! This plugin supports 1.8 or better, or 1.7.x with jQuery UI 1.8.24 -> http://jqueryui.com/resources/download/jquery-ui-1.8.24.zip") } } this.filter(':not(.'+opts.floatTableClass+')').each(function(){ var floatTheadId = util.uniqueId(); var $table = $(this); if($table.data('floatThead-attached')){ return true; //continue the each loop } if(!$table.is('table')){ throw new Error('jQuery.floatThead must be run on a table element. ex: $("table").floatThead();'); } canObserveMutations = opts.autoReflow && canObserveMutations; //option defaults to false! var $header = $table.children('thead:first'); var $tbody = $table.children('tbody:first'); if($header.length == 0 || $tbody.length == 0){ if(opts.debug) { if($header.length == 0){ debug('The thead element is missing.'); } else{ debug('The tbody element is missing.'); } } $table.data('floatThead-lazy', opts); $table.unbind("reflow").one('reflow', function(){ $table.floatThead(opts); }); return; } if($table.data('floatThead-lazy')){ $table.unbind("reflow"); } $table.data('floatThead-lazy', false); var headerFloated = true; var scrollingTop, scrollingBottom; var scrollbarOffset = {vertical: 0, horizontal: 0}; if(util.isFunction(scrollbarWidth)) { scrollbarWidth = scrollbarWidth(); } var lastColumnCount = 0; //used by columnNum() if(opts.scrollContainer === true){ opts.scrollContainer = getClosestScrollContainer; } var $scrollContainer = opts.scrollContainer($table) || $([]); //guard against returned nulls var locked = $scrollContainer.length > 0; var $responsiveContainer = locked ? $([]) : opts.responsiveContainer($table) || $([]); var responsive = isResponsiveContainerActive(); var useAbsolutePositioning = null; if (opts.position === 'auto') { useAbsolutePositioning = null; } else if (opts.position === 'fixed') { useAbsolutePositioning = false; } else if (opts.position === 'absolute'){ useAbsolutePositioning = true; } else if (opts.debug) { debug('Invalid value given to "position" option, valid is "fixed", "absolute" and "auto". You passed: ', opts.position); } if(useAbsolutePositioning == null){ //defaults: locked=true, !locked=false useAbsolutePositioning = locked; } var $caption = $table.find("caption"); var haveCaption = $caption.length == 1; if(haveCaption){ var captionAlignTop = ($caption.css("caption-side") || $caption.attr("align") || "top") === "top"; } var $fthGrp = $('').css({ 'display': 'table-footer-group', 'border-spacing': 0, 'height': 0, 'border-collapse': 'collapse', 'visibility': 'hidden' }); var wrappedContainer = false; //used with absolute positioning enabled. did we need to wrap the scrollContainer/table with a relative div? var $wrapper = $([]); //used when absolute positioning enabled - wraps the table and the float container var absoluteToFixedOnScroll = ieVersion <= 9 && !locked && useAbsolutePositioning; //on IE using absolute positioning doesn't look good with window scrolling, so we change position to fixed on scroll, and then change it back to absolute when done. var $floatTable = $("
').append( $('
').css('min-width', 100).text('X') ) ) ) ); $("body").append($test); var ret = ($test.find("table").width() == 0); $test.remove(); return ret; } return false; }; var createElements = !isFF && !ieVersion; //FF can read width from
"); var $floatColGroup = $(""); var $tableColGroup = $table.children('colgroup:first'); var existingColGroup = true; if($tableColGroup.length == 0){ $tableColGroup = $(""); existingColGroup = false; } var $fthRow = $('').css({ //created unstyled elements (used for sizing the table because chrome can't read width) 'display': 'table-row', 'border-spacing': 0, 'height': 0, 'border-collapse': 'collapse' }); var $floatContainer = $('
').css(opts.floatContainerCss).attr('aria-hidden', 'true'); var floatTableHidden = false; //this happens when the table is hidden and we do magic when making it visible var $newHeader = $("
"); var $sizerRow = $(''); var $sizerCells = $([]); var $tableCells = $([]); //used for sizing - either $sizerCells or $tableColGroup cols. $tableColGroup cols are only created in chrome for borderCollapse:collapse because of a chrome bug. var $headerCells = $([]); var $fthCells = $([]); //created elements $newHeader.append($sizerRow); $table.prepend($tableColGroup); if(createElements){ $fthGrp.append($fthRow); $table.append($fthGrp); } $floatTable.append($floatColGroup); $floatContainer.append($floatTable); if(opts.copyTableClass){ $floatTable.attr('class', $table.attr('class')); } $floatTable.attr({ //copy over some deprecated table attributes that people still like to use. Good thing people don't use colgroups... 'cellpadding': $table.attr('cellpadding'), 'cellspacing': $table.attr('cellspacing'), 'border': $table.attr('border') }); var tableDisplayCss = $table.css('display'); $floatTable.css({ 'borderCollapse': $table.css('borderCollapse'), 'border': $table.css('border'), 'display': tableDisplayCss }); if(!locked){ $floatTable.css('width', 'auto'); } if(tableDisplayCss === 'none'){ floatTableHidden = true; } $floatTable.addClass(opts.floatTableClass).css({'margin': 0, 'border-bottom-width': 0}); //must have no margins or you won't be able to click on things under floating table if(useAbsolutePositioning){ var makeRelative = function($container, alwaysWrap){ var positionCss = $container.css('position'); var relativeToScrollContainer = (positionCss == "relative" || positionCss == "absolute"); var $containerWrap = $container; if(!relativeToScrollContainer || alwaysWrap){ var css = {"paddingLeft": $container.css('paddingLeft'), "paddingRight": $container.css('paddingRight')}; $floatContainer.css(css); $containerWrap = $container.data('floatThead-containerWrap') || $container.wrap( $('
').addClass(opts.floatWrapperClass).css({ 'position': 'relative', 'clear': 'both' }) ).parent(); $container.data('floatThead-containerWrap', $containerWrap); //multiple tables inside one scrolling container - #242 wrappedContainer = true; } return $containerWrap; }; if(locked){ $wrapper = makeRelative($scrollContainer, true); $wrapper.prepend($floatContainer); } else { $wrapper = makeRelative($table); $table.before($floatContainer); } } else { $table.before($floatContainer); } $floatContainer.css({ position: useAbsolutePositioning ? 'absolute' : 'fixed', marginTop: 0, top: useAbsolutePositioning ? 0 : 'auto', zIndex: opts.zIndex, willChange: 'transform' }); $floatContainer.addClass(opts.floatContainerClass); updateScrollingOffsets(); var layoutFixed = {'table-layout': 'fixed'}; var layoutAuto = {'table-layout': $table.css('tableLayout') || 'auto'}; var originalTableWidth = $table[0].style.width || ""; //setting this to auto is bad: #70 var originalTableMinWidth = $table.css('minWidth') || ""; function eventName(name){ return name+'.fth-'+floatTheadId+'.floatTHead' } function setHeaderHeight(){ var headerHeight = 0; $header.children("tr:visible").each(function(){ headerHeight += $(this).outerHeight(true); }); if($table.css('border-collapse') == 'collapse') { var tableBorderTopHeight = parseInt($table.css('border-top-width'), 10); var cellBorderTopHeight = parseInt($table.find("thead tr:first").find(">*:first").css('border-top-width'), 10); if(tableBorderTopHeight > cellBorderTopHeight) { headerHeight -= (tableBorderTopHeight / 2); //id love to see some docs where this magic recipe is found.. } } $sizerRow.outerHeight(headerHeight); $sizerCells.outerHeight(headerHeight); } function setFloatWidth(){ var tw = tableWidth($table, $fthCells, true); var $container = responsive ? $responsiveContainer : $scrollContainer; var width = $container.length ? getOffsetWidth($container[0]) : tw; var floatContainerWidth = $container.css("overflow-y") != 'hidden' ? width - scrollbarOffset.vertical : width; $floatContainer.width(floatContainerWidth); if(locked){ var percent = 100 * tw / (floatContainerWidth); $floatTable.css('width', percent+'%'); } else { $floatTable.outerWidth(tw); } } function updateScrollingOffsets(){ scrollingTop = (util.isFunction(opts.top) ? opts.top($table) : opts.top) || 0; scrollingBottom = (util.isFunction(opts.bottom) ? opts.bottom($table) : opts.bottom) || 0; } /** * get the number of columns and also rebuild resizer rows if the count is different than the last count */ function columnNum(){ var count; var $headerColumns = $header.find(opts.headerCellSelector); if(existingColGroup){ count = $tableColGroup.find('col').length; } else { count = 0; $headerColumns.each(function () { count += parseInt(($(this).attr('colspan') || 1), 10); }); } if(count !== lastColumnCount){ lastColumnCount = count; var cells = [], cols = [], psuedo = [], content; for(var x = 0; x < count; x++){ content = $headerColumns.eq(x).text(); cells.push('
'); psuedo.push( $('').css({ 'display': 'table-cell', 'height': 0, 'width': 'auto' }) ); } cols = cols.join(''); cells = cells.join(''); if(createElements){ $fthRow.empty(); $fthRow.append(psuedo); $fthCells = $fthRow.find('fthtd'); } $sizerRow.html(cells); $sizerCells = $sizerRow.find("th"); if(!existingColGroup){ $tableColGroup.html(cols); } $tableCells = $tableColGroup.find('col'); $floatColGroup.html(cols); $headerCells = $floatColGroup.find("col"); } return count; } function refloat(){ //make the thing float if(!headerFloated){ headerFloated = true; if(useAbsolutePositioning){ //#53, #56 var tw = tableWidth($table, $fthCells, true); var wrapperWidth = $wrapper.width(); if(tw > wrapperWidth){ $table.css('minWidth', tw); } } $table.css(layoutFixed); $floatTable.css(layoutFixed); $floatTable.append($header); //append because colgroup must go first in chrome $tbody.before($newHeader); setHeaderHeight(); } } function unfloat(){ //put the header back into the table if(headerFloated){ headerFloated = false; if(useAbsolutePositioning){ //#53, #56 $table.width(originalTableWidth); } $newHeader.detach(); $table.prepend($header); $table.css(layoutAuto); $floatTable.css(layoutAuto); $table.css('minWidth', originalTableMinWidth); //this looks weird, but it's not a bug. Think about it!! $table.css('minWidth', tableWidth($table, $fthCells)); //#121 } } var isHeaderFloatingLogical = false; //for the purpose of this event, the header is/isnt floating, even though the element //might be in some other state. this is what the header looks like to the user function triggerFloatEvent(isFloating){ if(isHeaderFloatingLogical != isFloating){ isHeaderFloatingLogical = isFloating; $table.triggerHandler("floatThead", [isFloating, $floatContainer]) } } function changePositioning(isAbsolute){ if(useAbsolutePositioning != isAbsolute){ useAbsolutePositioning = isAbsolute; $floatContainer.css({ position: useAbsolutePositioning ? 'absolute' : 'fixed' }); } } function getSizingRow($table, $cols, $fthCells, ieVersion){ if(createElements){ return $fthCells; } else if(ieVersion) { return opts.getSizingRow($table, $cols, $fthCells); } else { return $cols; } } /** * returns a function that updates the floating header's cell widths. * @return {Function} */ function reflow(){ var i; var numCols = columnNum(); //if the tables columns changed dynamically since last time (datatables), rebuild the sizer rows and get a new count return function(){ //Cache the current scrollLeft value so that it can be reset post reflow var scrollLeft = $floatContainer.scrollLeft(); $tableCells = $tableColGroup.find('col'); var $rowCells = getSizingRow($table, $tableCells, $fthCells, ieVersion); if($rowCells.length == numCols && numCols > 0){ if(!existingColGroup){ for(i=0; i < numCols; i++){ $tableCells.eq(i).css('width', ''); } } unfloat(); var widths = []; for(i=0; i < numCols; i++){ widths[i] = getOffsetWidth($rowCells.get(i)); } for(i=0; i < numCols; i++){ $headerCells.eq(i).width(widths[i]); $tableCells.eq(i).width(widths[i]); } refloat(); } else { $floatTable.append($header); $table.css(layoutAuto); $floatTable.css(layoutAuto); setHeaderHeight(); } //Set back the current scrollLeft value on floatContainer $floatContainer.scrollLeft(scrollLeft); $table.triggerHandler("reflowed", [$floatContainer]); }; } function floatContainerBorderWidth(side){ var border = $scrollContainer.css("border-"+side+"-width"); var w = 0; if (border && ~border.indexOf('px')) { w = parseInt(border, 10); } return w; } function isResponsiveContainerActive(){ return $responsiveContainer.css("overflow-x") == 'auto'; } /** * first performs initial calculations that we expect to not change when the table, window, or scrolling container are scrolled. * returns a function that calculates the floating container's top and left coords. takes into account if we are using page scrolling or inner scrolling * @return {Function} */ function calculateFloatContainerPosFn(){ var scrollingContainerTop = $scrollContainer.scrollTop(); //this floatEnd calc was moved out of the returned function because we assume the table height doesn't change (otherwise we must reinit by calling calculateFloatContainerPosFn) var floatEnd; var tableContainerGap = 0; var captionHeight = haveCaption ? $caption.outerHeight(true) : 0; var captionScrollOffset = captionAlignTop ? captionHeight : -captionHeight; var floatContainerHeight = $floatContainer.height(); var tableOffset = $table.offset(); var tableLeftGap = 0; //can be caused by border on container (only in locked mode) var tableTopGap = 0; if(locked){ var containerOffset = $scrollContainer.offset(); tableContainerGap = tableOffset.top - containerOffset.top + scrollingContainerTop; if(haveCaption && captionAlignTop){ tableContainerGap += captionHeight; } tableLeftGap = floatContainerBorderWidth('left'); tableTopGap = floatContainerBorderWidth('top'); tableContainerGap -= tableTopGap; } else { floatEnd = tableOffset.top - scrollingTop - floatContainerHeight + scrollingBottom + scrollbarOffset.horizontal; } var windowTop = $window.scrollTop(); var windowLeft = $window.scrollLeft(); var getScrollContainerLeft = function(){ return (isResponsiveContainerActive() ? $responsiveContainer : $scrollContainer).scrollLeft() || 0; }; var scrollContainerLeft = getScrollContainerLeft(); return function(eventType){ responsive = isResponsiveContainerActive(); var isTableHidden = $table[0].offsetWidth <= 0 && $table[0].offsetHeight <= 0; if(!isTableHidden && floatTableHidden) { floatTableHidden = false; setTimeout(function(){ $table.triggerHandler("reflow"); }, 1); return null; } if(isTableHidden){ //it's hidden floatTableHidden = true; if(!useAbsolutePositioning){ return null; } } if(eventType == 'windowScroll'){ windowTop = $window.scrollTop(); windowLeft = $window.scrollLeft(); } else if(eventType == 'containerScroll'){ if($responsiveContainer.length){ if(!responsive){ return; //we dont care about the event if we arent responsive right now } scrollContainerLeft = $responsiveContainer.scrollLeft(); } else { scrollingContainerTop = $scrollContainer.scrollTop(); scrollContainerLeft = $scrollContainer.scrollLeft(); } } else if(eventType != 'init') { windowTop = $window.scrollTop(); windowLeft = $window.scrollLeft(); scrollingContainerTop = $scrollContainer.scrollTop(); scrollContainerLeft = getScrollContainerLeft(); } if(isWebkit && (windowTop < 0 || windowLeft < 0)){ //chrome overscroll effect at the top of the page - breaks fixed positioned floated headers return; } if(absoluteToFixedOnScroll){ if(eventType == 'windowScrollDone'){ changePositioning(true); //change to absolute } else { changePositioning(false); //change to fixed } } else if(eventType == 'windowScrollDone'){ return null; //event is fired when they stop scrolling. ignore it if not 'absoluteToFixedOnScroll' } tableOffset = $table.offset(); if(haveCaption && captionAlignTop){ tableOffset.top += captionHeight; } var top, left; var tableHeight = $table.outerHeight(); if(locked && useAbsolutePositioning){ //inner scrolling, absolute positioning if (tableContainerGap >= scrollingContainerTop) { var gap = tableContainerGap - scrollingContainerTop + tableTopGap; top = gap > 0 ? gap : 0; triggerFloatEvent(false); } else { top = wrappedContainer ? tableTopGap : scrollingContainerTop; //headers stop at the top of the viewport triggerFloatEvent(true); } left = tableLeftGap; } else if(!locked && useAbsolutePositioning) { //window scrolling, absolute positioning if(windowTop > floatEnd + tableHeight + captionScrollOffset){ top = tableHeight - floatContainerHeight + captionScrollOffset; //scrolled past table } else if (tableOffset.top >= windowTop + scrollingTop) { top = 0; //scrolling to table unfloat(); triggerFloatEvent(false); } else { top = scrollingTop + windowTop - tableOffset.top + tableContainerGap + (captionAlignTop ? captionHeight : 0); refloat(); //scrolling within table. header floated triggerFloatEvent(true); } left = scrollContainerLeft; } else if(locked && !useAbsolutePositioning){ //inner scrolling, fixed positioning if (tableContainerGap > scrollingContainerTop || scrollingContainerTop - tableContainerGap > tableHeight) { top = tableOffset.top - windowTop; unfloat(); triggerFloatEvent(false); } else { top = tableOffset.top + scrollingContainerTop - windowTop - tableContainerGap; refloat(); triggerFloatEvent(true); //headers stop at the top of the viewport } left = tableOffset.left + scrollContainerLeft - windowLeft; } else if(!locked && !useAbsolutePositioning) { //window scrolling, fixed positioning if(windowTop > floatEnd + tableHeight + captionScrollOffset){ top = tableHeight + scrollingTop - windowTop + floatEnd + captionScrollOffset; //scrolled past the bottom of the table } else if (tableOffset.top > windowTop + scrollingTop) { top = tableOffset.top - windowTop; refloat(); triggerFloatEvent(false); //this is a weird case, the header never gets unfloated and i have no no way to know //scrolled past the top of the table } else { //scrolling within the table top = scrollingTop; triggerFloatEvent(true); } left = tableOffset.left + scrollContainerLeft - windowLeft; } return {top: Math.round(top), left: Math.round(left)}; }; } /** * returns a function that caches old floating container position and only updates css when the position changes * @return {Function} */ function repositionFloatContainerFn(){ var oldTop = null; var oldLeft = null; var oldScrollLeft = null; return function(pos, setWidth, setHeight){ if(pos != null && (oldTop != pos.top || oldLeft != pos.left)){ if(ieVersion === 8){ $floatContainer.css({ top: pos.top, left: pos.left }); } else { var transform = 'translateX(' + pos.left + 'px) translateY(' + pos.top + 'px)'; var cssObj = { '-webkit-transform' : transform, '-moz-transform' : transform, '-ms-transform' : transform, '-o-transform' : transform, 'transform' : transform, 'top': 0, }; cssObj[/rtl/i.test(document.documentElement.dir || '') ? 'right': 'left'] = 0; $floatContainer.css(cssObj); } oldTop = pos.top; oldLeft = pos.left; } if(setWidth){ setFloatWidth(); } if(setHeight){ setHeaderHeight(); } var scrollLeft = (responsive ? $responsiveContainer : $scrollContainer).scrollLeft(); if(!useAbsolutePositioning || oldScrollLeft != scrollLeft){ $floatContainer.scrollLeft(scrollLeft); oldScrollLeft = scrollLeft; } } } /** * checks if THIS table has scrollbars, and finds their widths */ function calculateScrollBarSize(){ //this should happen after the floating table has been positioned if($scrollContainer.length){ if(opts.support && opts.support.perfectScrollbar && $scrollContainer.data().perfectScrollbar){ scrollbarOffset = {horizontal:0, vertical:0}; } else { if($scrollContainer.css('overflow-x') == 'scroll'){ scrollbarOffset.horizontal = scrollbarWidth; } else { var sw = $scrollContainer.width(), tw = tableWidth($table, $fthCells); var offsetv = sh < th ? scrollbarWidth : 0; scrollbarOffset.horizontal = sw - offsetv < tw ? scrollbarWidth : 0; } if($scrollContainer.css('overflow-y') == 'scroll'){ scrollbarOffset.vertical = scrollbarWidth; } else { var sh = $scrollContainer.height(), th = $table.height(); var offseth = sw < tw ? scrollbarWidth : 0; scrollbarOffset.vertical = sh - offseth < th ? scrollbarWidth : 0; } } } } //finish up. create all calculation functions and bind them to events calculateScrollBarSize(); var flow; var ensureReflow = function(){ flow = reflow(); flow(); }; ensureReflow(); var calculateFloatContainerPos = calculateFloatContainerPosFn(); var repositionFloatContainer = repositionFloatContainerFn(); repositionFloatContainer(calculateFloatContainerPos('init'), true); //this must come after reflow because reflow changes scrollLeft back to 0 when it rips out the thead var windowScrollDoneEvent = util.debounce(function(){ repositionFloatContainer(calculateFloatContainerPos('windowScrollDone'), false); }, 1); var windowScrollEvent = function(){ repositionFloatContainer(calculateFloatContainerPos('windowScroll'), false); if(absoluteToFixedOnScroll){ windowScrollDoneEvent(); } }; var containerScrollEvent = function(){ repositionFloatContainer(calculateFloatContainerPos('containerScroll'), false); }; var windowResizeEvent = function(){ if($table.is(":hidden")){ return; } updateScrollingOffsets(); calculateScrollBarSize(); ensureReflow(); calculateFloatContainerPos = calculateFloatContainerPosFn(); repositionFloatContainer = repositionFloatContainerFn(); repositionFloatContainer(calculateFloatContainerPos('resize'), true, true); }; var reflowEvent = util.debounce(function(){ if($table.is(":hidden")){ return; } calculateScrollBarSize(); updateScrollingOffsets(); ensureReflow(); calculateFloatContainerPos = calculateFloatContainerPosFn(); repositionFloatContainer(calculateFloatContainerPos('reflow'), true, true); }, 1); /////// printing stuff var beforePrint = function(){ unfloat(); }; var afterPrint = function(){ refloat(); }; var printEvent = function(mql){ //make printing the table work properly on IE10+ if(mql.matches) { beforePrint(); } else { afterPrint(); } }; var matchMediaPrint = null; if(window.matchMedia && window.matchMedia('print').addListener && !buggyMatchMedia){ matchMediaPrint = window.matchMedia("print"); matchMediaPrint.addListener(printEvent); } else { $window.on('beforeprint', beforePrint); $window.on('afterprint', afterPrint); } ////// end printing stuff if(locked){ //internal scrolling if(useAbsolutePositioning){ $scrollContainer.on(eventName('scroll'), containerScrollEvent); } else { $scrollContainer.on(eventName('scroll'), containerScrollEvent); $window.on(eventName('scroll'), windowScrollEvent); } } else { //window scrolling $responsiveContainer.on(eventName('scroll'), containerScrollEvent); $window.on(eventName('scroll'), windowScrollEvent); } $window.on(eventName('load'), reflowEvent); //for tables with images windowResize(eventName('resize'), windowResizeEvent); $table.on('reflow', reflowEvent); if(opts.support && opts.support.datatables && isDatatable($table)){ $table .on('filter', reflowEvent) .on('sort', reflowEvent) .on('page', reflowEvent); } if(opts.support && opts.support.bootstrap) { $window.on(eventName('shown.bs.tab'), reflowEvent); // people cant seem to figure out how to use this plugin with bs3 tabs... so this :P } if(opts.support && opts.support.jqueryUI) { $window.on(eventName('tabsactivate'), reflowEvent); // same thing for jqueryui } if (canObserveMutations) { var mutationElement = null; if(util.isFunction(opts.autoReflow)){ mutationElement = opts.autoReflow($table, $scrollContainer) } if(!mutationElement) { mutationElement = $scrollContainer.length ? $scrollContainer[0] : $table[0] } mObs = new MutationObserver(function(e){ var wasTableRelated = function(nodes){ return nodes && nodes[0] && (nodes[0].nodeName == "THEAD" || nodes[0].nodeName == "TD"|| nodes[0].nodeName == "TH"); }; for(var i=0; i < e.length; i++){ if(!(wasTableRelated(e[i].addedNodes) || wasTableRelated(e[i].removedNodes))){ reflowEvent(); break; } } }); mObs.observe(mutationElement, { childList: true, subtree: true }); } //attach some useful functions to the table. $table.data('floatThead-attached', { destroy: function(){ var ns = '.fth-'+floatTheadId; unfloat(); $table.css(layoutAuto); $tableColGroup.remove(); createElements && $fthGrp.remove(); if($newHeader.parent().length){ //only if it's in the DOM $newHeader.replaceWith($header); } triggerFloatEvent(false); if(canObserveMutations){ mObs.disconnect(); mObs = null; } $table.off('reflow reflowed'); $scrollContainer.off(ns); $responsiveContainer.off(ns); if (wrappedContainer) { if ($scrollContainer.length) { $scrollContainer.unwrap(); } else { $table.unwrap(); } } if(locked){ $scrollContainer.data('floatThead-containerWrap', false); } else { $table.data('floatThead-containerWrap', false); } $table.css('minWidth', originalTableMinWidth); $floatContainer.remove(); $table.data('floatThead-attached', false); $window.off(ns); if (matchMediaPrint) { matchMediaPrint.removeListener(printEvent); } beforePrint = afterPrint = function(){}; return function reinit(){ return $table.floatThead(opts); } }, reflow: function(){ reflowEvent(); }, setHeaderHeight: function(){ setHeaderHeight(); }, getFloatContainer: function(){ return $floatContainer; }, getRowGroups: function(){ if(headerFloated){ return $floatContainer.find('>table>thead').add($table.children("tbody,tfoot")); } else { return $table.children("thead,tbody,tfoot"); } } }); }); return this; }; })((function(){ var $ = window.jQuery; if(typeof module !== 'undefined' && module.exports && !$) { // only use cjs if they dont have a jquery for me to use, and we have commonjs $ = require('jquery'); } return $; })());