jquery.stickytableheaders.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. /*! Copyright (c) 2011 by Jonas Mosbech - https://github.com/jmosbech/StickyTableHeaders
  2. MIT license info: https://github.com/jmosbech/StickyTableHeaders/blob/master/license.txt */
  3. ;(function ($, window, undefined) {
  4. 'use strict';
  5. var name = 'stickyTableHeaders',
  6. id = 0,
  7. defaults = {
  8. fixedOffset: 0,
  9. leftOffset: 0,
  10. scrollableArea: window
  11. };
  12. function Plugin (el, options) {
  13. // To avoid scope issues, use 'base' instead of 'this'
  14. // to reference this class from internal events and functions.
  15. var base = this;
  16. // Access to jQuery and DOM versions of element
  17. base.$el = $(el);
  18. base.el = el;
  19. base.id = id++;
  20. // Listen for destroyed, call teardown
  21. base.$el.bind('destroyed',
  22. $.proxy(base.teardown, base));
  23. // Cache DOM refs for performance reasons
  24. base.$clonedHeader = null;
  25. base.$originalHeader = null;
  26. // Keep track of state
  27. base.isSticky = false;
  28. base.hasBeenSticky = false;
  29. base.leftOffset = null;
  30. base.topOffset = null;
  31. base.init = function () {
  32. base.options = $.extend({}, defaults, options);
  33. base.$el.each(function () {
  34. var $this = $(this);
  35. // remove padding on <table> to fix issue #7
  36. $this.css('padding', 0);
  37. base.$scrollableArea = $(base.options.scrollableArea);
  38. base.$originalHeader = $('thead:first', this);
  39. base.$clonedHeader = base.$originalHeader.clone();
  40. $this.trigger('clonedHeader.' + name, [base.$clonedHeader]);
  41. base.$clonedHeader.addClass('tableFloatingHeader');
  42. base.$clonedHeader.css('display', 'none');
  43. base.$originalHeader.addClass('tableFloatingHeaderOriginal');
  44. base.$originalHeader.after(base.$clonedHeader);
  45. base.$printStyle = $('<style type="text/css" media="print">' +
  46. '.tableFloatingHeader{display:none !important;}' +
  47. '.tableFloatingHeaderOriginal{position:static !important;}' +
  48. '</style>');
  49. $('head').append(base.$printStyle);
  50. });
  51. base.updateWidth();
  52. base.toggleHeaders();
  53. base.bind();
  54. };
  55. base.destroy = function (){
  56. base.$el.unbind('destroyed', base.teardown);
  57. base.teardown();
  58. };
  59. base.teardown = function(){
  60. if (base.isSticky) {
  61. base.$originalHeader.css('position', 'static');
  62. }
  63. $.removeData(base.el, 'plugin_' + name);
  64. base.unbind();
  65. base.$clonedHeader.remove();
  66. base.$originalHeader.removeClass('tableFloatingHeaderOriginal');
  67. base.$originalHeader.css('visibility', 'visible');
  68. base.$printStyle.remove();
  69. base.el = null;
  70. base.$el = null;
  71. };
  72. base.bind = function(){
  73. base.$scrollableArea.on('scroll.' + name, base.toggleHeaders);
  74. if (!base.isWindowScrolling()) {
  75. $(window).on('scroll.' + name + base.id, base.setPositionValues);
  76. $(window).on('resize.' + name + base.id, base.toggleHeaders);
  77. }
  78. base.$scrollableArea.on('resize.' + name, base.toggleHeaders);
  79. base.$scrollableArea.on('resize.' + name, base.updateWidth);
  80. };
  81. base.unbind = function(){
  82. // unbind window events by specifying handle so we don't remove too much
  83. base.$scrollableArea.off('.' + name, base.toggleHeaders);
  84. if (!base.isWindowScrolling()) {
  85. $(window).off('.' + name + base.id, base.setPositionValues);
  86. $(window).off('.' + name + base.id, base.toggleHeaders);
  87. }
  88. base.$scrollableArea.off('.' + name, base.updateWidth);
  89. base.$el.off('.' + name);
  90. base.$el.find('*').off('.' + name);
  91. };
  92. base.toggleHeaders = function () {
  93. if (base.$el) {
  94. base.$el.each(function () {
  95. var $this = $(this),
  96. newLeft,
  97. newTopOffset = base.isWindowScrolling() ? (
  98. isNaN(base.options.fixedOffset) ?
  99. base.options.fixedOffset.outerHeight() :
  100. base.options.fixedOffset
  101. ) :
  102. base.$scrollableArea.offset().top + (!isNaN(base.options.fixedOffset) ? base.options.fixedOffset : 0),
  103. offset = $this.offset(),
  104. scrollTop = base.$scrollableArea.scrollTop() + newTopOffset,
  105. scrollLeft = base.$scrollableArea.scrollLeft(),
  106. scrolledPastTop = base.isWindowScrolling() ?
  107. scrollTop > offset.top :
  108. newTopOffset > offset.top,
  109. notScrolledPastBottom = (base.isWindowScrolling() ? scrollTop : 0) <
  110. (offset.top + $this.height() - base.$clonedHeader.height() - (base.isWindowScrolling() ? 0 : newTopOffset));
  111. if (scrolledPastTop && notScrolledPastBottom) {
  112. newLeft = offset.left - scrollLeft + base.options.leftOffset;
  113. base.$originalHeader.css({
  114. 'position': 'fixed',
  115. 'margin-top': 0,
  116. 'left': newLeft,
  117. 'z-index': 1 // #18: opacity bug
  118. });
  119. base.isSticky = true;
  120. base.leftOffset = newLeft;
  121. base.topOffset = newTopOffset;
  122. base.$clonedHeader.css('display', '');
  123. base.setPositionValues();
  124. // make sure the width is correct: the user might have resized the browser while in static mode
  125. base.updateWidth();
  126. } else if (base.isSticky) {
  127. base.$originalHeader.css('position', 'static');
  128. base.$clonedHeader.css('display', 'none');
  129. base.isSticky = false;
  130. base.resetWidth($("td,th", base.$clonedHeader), $("td,th", base.$originalHeader));
  131. }
  132. });
  133. }
  134. };
  135. base.isWindowScrolling = function() {
  136. return base.$scrollableArea[0] === window;
  137. };
  138. base.setPositionValues = function () {
  139. var winScrollTop = $(window).scrollTop(),
  140. winScrollLeft = $(window).scrollLeft();
  141. if (!base.isSticky ||
  142. winScrollTop < 0 || winScrollTop + $(window).height() > $(document).height() ||
  143. winScrollLeft < 0 || winScrollLeft + $(window).width() > $(document).width()) {
  144. return;
  145. }
  146. base.$originalHeader.css({
  147. 'top': base.topOffset - (base.isWindowScrolling() ? 0 : winScrollTop),
  148. 'left': base.leftOffset - (base.isWindowScrolling() ? 0 : winScrollLeft)
  149. });
  150. };
  151. base.updateWidth = function () {
  152. if (!base.isSticky) {
  153. return;
  154. }
  155. // Copy cell widths from clone
  156. if (!base.$originalHeaderCells) {
  157. base.$originalHeaderCells = $('th,td', base.$originalHeader);
  158. }
  159. if (!base.$clonedHeaderCells) {
  160. base.$clonedHeaderCells = $('th,td', base.$clonedHeader);
  161. }
  162. var cellWidths = base.getWidth(base.$clonedHeaderCells);
  163. base.setWidth(cellWidths, base.$clonedHeaderCells, base.$originalHeaderCells);
  164. // Copy row width from whole table
  165. base.$originalHeader.css('width', base.$clonedHeader.width());
  166. };
  167. base.getWidth = function ($clonedHeaders) {
  168. var widths = [];
  169. $clonedHeaders.each(function (index) {
  170. var width, $this = $(this);
  171. if ($this.css('box-sizing') === 'border-box') {
  172. width = $this.outerWidth(); // #39: border-box bug
  173. } else {
  174. width = $this.width();
  175. }
  176. widths[index] = width;
  177. });
  178. return widths;
  179. };
  180. base.setWidth = function (widths, $clonedHeaders, $origHeaders) {
  181. $clonedHeaders.each(function (index) {
  182. var width = widths[index];
  183. $origHeaders.eq(index).css({
  184. 'min-width': width,
  185. 'max-width': width
  186. });
  187. });
  188. };
  189. base.resetWidth = function ($clonedHeaders, $origHeaders) {
  190. $clonedHeaders.each(function (index) {
  191. var $this = $(this);
  192. $origHeaders.eq(index).css({
  193. 'min-width': $this.css("min-width"),
  194. 'max-width': $this.css("max-width")
  195. });
  196. });
  197. };
  198. base.updateOptions = function(options) {
  199. base.options = $.extend({}, defaults, options);
  200. base.updateWidth();
  201. base.toggleHeaders();
  202. };
  203. // Run initializer
  204. base.init();
  205. }
  206. // A plugin wrapper around the constructor,
  207. // preventing against multiple instantiations
  208. $.fn[name] = function ( options ) {
  209. return this.each(function () {
  210. var instance = $.data(this, 'plugin_' + name);
  211. if (instance) {
  212. if (typeof options === "string") {
  213. instance[options].apply(instance);
  214. } else {
  215. instance.updateOptions(options);
  216. }
  217. } else if(options !== 'destroy') {
  218. $.data(this, 'plugin_' + name, new Plugin( this, options ));
  219. }
  220. });
  221. };
  222. })(jQuery, window);