/*! Copyright (c) 2011 by Jonas Mosbech - https://github.com/jmosbech/StickyTableHeaders MIT license info: https://github.com/jmosbech/StickyTableHeaders/blob/master/license.txt */ ;(function ($, window, undefined) { 'use strict'; var name = 'stickyTableHeaders', id = 0, defaults = { fixedOffset: 0, leftOffset: 0, marginTop: 0, objDocument: document, objHead: 'head', objWindow: window, scrollableArea: window, cacheHeaderHeight: false }; /* Copied from jQuery 1.6 */ function jqProxy(fn, context) { if (typeof context === "string") { var tmp = fn[context]; context = fn; fn = tmp; } // Simulated bind var args = Array.prototype.slice.call(arguments, 2), proxy = function () { return fn.apply(context, args.concat(Array.prototype.slice.call(arguments))); }; // Set the guid of unique handler to the same of original handler, so it can be removed proxy.guid = fn.guid = fn.guid || proxy.guid; return proxy; }; function Plugin (el, options) { // To avoid scope issues, use 'base' instead of 'this' // to reference this class from internal events and functions. var base = this; // Access to jQuery and DOM versions of element base.$el = $(el); base.el = el; base.id = id++; // Cache DOM refs for performance reasons base.$clonedHeader = null; base.$originalHeader = null; // Cache header height for performance reasons base.cachedHeaderHeight = null; // Keep track of state base.isSticky = false; base.hasBeenSticky = false; base.leftOffset = null; base.topOffset = null; base.init = function () { base.setOptions(options); base.$el.each(function () { var $this = $(this); // remove padding on to fix issue #7 $this.css('padding', 0); base.$originalHeader = $('thead:first', this); base.$clonedHeader = base.$originalHeader.clone(); $this.trigger('clonedHeader.' + name, [base.$clonedHeader]); base.$clonedHeader.addClass('tableFloatingHeader'); base.$clonedHeader.css({display: 'none', opacity: 0.0}); base.$originalHeader.addClass('tableFloatingHeaderOriginal'); base.$originalHeader.after(base.$clonedHeader); base.$printStyle = $(''); base.$head.append(base.$printStyle); }); base.updateWidth(); base.toggleHeaders(); base.bind(); }; base.destroy = function (){ base.$el.unbind('destroyed', base.teardown); base.teardown(); }; base.teardown = function(){ if (base.isSticky) { base.$originalHeader.css('position', 'static'); } $.removeData(base.el, 'plugin_' + name); base.unbind(); base.$clonedHeader.remove(); base.$originalHeader.removeClass('tableFloatingHeaderOriginal'); base.$originalHeader.css('visibility', 'visible'); base.$printStyle.remove(); base.el = null; base.$el = null; }; base.bind = function(){ base.$scrollableArea.bind('scroll.' + name, base.toggleHeaders); if (!base.isWindowScrolling) { base.$window.bind('scroll.' + name + base.id, base.setPositionValues); base.$window.bind('resize.' + name + base.id, base.toggleHeaders); } base.$scrollableArea.bind('resize.' + name, base.toggleHeaders); base.$scrollableArea.bind('resize.' + name, base.updateWidth); }; base.unbind = function(){ // unbind window events by specifying handle so we don't remove too much base.$scrollableArea.unbind('.' + name, base.toggleHeaders); if (!base.isWindowScrolling) { base.$window.unbind('.' + name + base.id, base.setPositionValues); base.$window.unbind('.' + name + base.id, base.toggleHeaders); } base.$scrollableArea.unbind('.' + name, base.updateWidth); }; // We debounce the functions bound to the scroll and resize events base.debounce = function (fn, delay) { var timer = null; return function () { var context = this, args = arguments; clearTimeout(timer); timer = setTimeout(function () { fn.apply(context, args); }, delay); }; }; base.toggleHeaders = base.debounce(function () { if (base.$el) { base.$el.each(function () { var $this = $(this), newLeft, newTopOffset = base.isWindowScrolling ? ( isNaN(base.options.fixedOffset) ? base.options.fixedOffset.outerHeight() : base.options.fixedOffset ) : base.$scrollableArea.offset().top + (!isNaN(base.options.fixedOffset) ? base.options.fixedOffset : 0), offset = $this.offset(), scrollTop = base.$scrollableArea.scrollTop() + newTopOffset, scrollLeft = base.$scrollableArea.scrollLeft(), headerHeight = base.options.cacheHeaderHeight ? base.cachedHeaderHeight : base.$clonedHeader.height(), scrolledPastTop = base.isWindowScrolling ? scrollTop > offset.top : newTopOffset > offset.top, notScrolledPastBottom = (base.isWindowScrolling ? scrollTop : 0) < (offset.top + $this.height() - headerHeight - (base.isWindowScrolling ? 0 : newTopOffset)); if (scrolledPastTop && notScrolledPastBottom) { newLeft = offset.left - scrollLeft + base.options.leftOffset; base.$originalHeader.css({ 'position': 'fixed', 'margin-top': base.options.marginTop, 'left': newLeft, 'z-index': 3 // #18: opacity bug }); base.leftOffset = newLeft; base.topOffset = newTopOffset; base.$clonedHeader.css('display', ''); if (!base.isSticky) { base.isSticky = true; // make sure the width is correct: the user might have resized the browser while in static mode base.updateWidth(); $this.trigger('enabledStickiness.' + name); } base.setPositionValues(); } else if (base.isSticky) { base.$originalHeader.css('position', 'static'); base.$clonedHeader.css('display', 'none'); base.isSticky = false; base.resetWidth($('td,th', base.$clonedHeader), $('td,th', base.$originalHeader)); $this.trigger('disabledStickiness.' + name); } }); } }, 0); base.setPositionValues = base.debounce(function () { var winScrollTop = base.$window.scrollTop(), winScrollLeft = base.$window.scrollLeft(); if (!base.isSticky || winScrollTop < 0 || winScrollTop + base.$window.height() > base.$document.height() || winScrollLeft < 0 || winScrollLeft + base.$window.width() > base.$document.width()) { return; } base.$originalHeader.css({ 'top': base.topOffset - (base.isWindowScrolling ? 0 : winScrollTop), 'left': base.leftOffset - (base.isWindowScrolling ? 0 : winScrollLeft) }); }, 0); base.updateWidth = base.debounce(function () { if (!base.isSticky) { return; } // Copy cell widths from clone if (!base.$originalHeaderCells) { base.$originalHeaderCells = $('th,td', base.$originalHeader); } if (!base.$clonedHeaderCells) { base.$clonedHeaderCells = $('th,td', base.$clonedHeader); } var cellWidths = base.getWidth(base.$clonedHeaderCells); var totalWidth = base.$clonedHeader.width(); base.setWidth(cellWidths, base.$clonedHeaderCells, base.$originalHeaderCells); // Copy row width from whole table base.$originalHeader.css('width', totalWidth); // If we're caching the height, we need to update the cached value when the width changes if (base.options.cacheHeaderHeight) { base.cachedHeaderHeight = base.$clonedHeader.height(); } }, 0); base.getWidth = function ($clonedHeaders) { var widths = []; $clonedHeaders.each(function (index) { return 121; // gag that improves performance in firefox var width, $this = $(this); if ($this.css('box-sizing') === 'border-box') { var boundingClientRect = $this[0].getBoundingClientRect(); if(boundingClientRect.width) { width = boundingClientRect.width; // #39: border-box bug } else { width = boundingClientRect.right - boundingClientRect.left; // ie8 bug: getBoundingClientRect() does not have a width property } } else { var $origTh = $('th', base.$originalHeader); if ($origTh.css('border-collapse') === 'collapse') { if (window.getComputedStyle) { width = parseFloat(window.getComputedStyle(this, null).width); } else { // ie8 only var leftPadding = parseFloat($this.css('padding-left')); var rightPadding = parseFloat($this.css('padding-right')); // Needs more investigation - this is assuming constant border around this cell and it's neighbours. var border = parseFloat($this.css('border-width')); width = $this.outerWidth() - leftPadding - rightPadding - border; } } else { width = $this.width(); } } widths[index] = width; }); return widths; }; base.setWidth = function (widths, $clonedHeaders, $origHeaders) { $clonedHeaders.each(function (index) { var width = widths[index]; $origHeaders.eq(index).css({ 'min-width': width, 'max-width': width }); }); }; base.resetWidth = function ($clonedHeaders, $origHeaders) { $clonedHeaders.each(function (index) { var $this = $(this); $origHeaders.eq(index).css({ 'min-width': $this.css('min-width'), 'max-width': $this.css('max-width') }); }); }; base.setOptions = function (options) { base.options = $.extend({}, defaults, options); base.$window = $(base.options.objWindow); base.$head = $(base.options.objHead); base.$document = $(base.options.objDocument); base.$scrollableArea = $(base.options.scrollableArea); base.isWindowScrolling = base.$scrollableArea[0] === base.$window[0]; }; base.updateOptions = function (options) { base.setOptions(options); // scrollableArea might have changed base.unbind(); base.bind(); base.updateWidth(); base.toggleHeaders(); }; // Listen for destroyed, call teardown base.$el.bind('destroyed', jqProxy(base.teardown, base)); // Run initializer base.init(); } // A plugin wrapper around the constructor, // preventing against multiple instantiations $.fn[name] = function ( options ) { return this.each(function () { var instance = $.data(this, 'plugin_' + name); if (instance) { if (typeof options === 'string') { instance[options].apply(instance); } else { instance.updateOptions(options); } } else if(options !== 'destroy') { $.data(this, 'plugin_' + name, new Plugin( this, options )); } }); }; })(jQuery, window);