jquery.floatthead.js 41 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066
  1. // @preserve jQuery.floatThead 1.4.0 - http://mkoryak.github.io/floatThead/ - Copyright (c) 2012 - 2016 Misha Koryak
  2. // @license MIT
  3. /* @author Misha Koryak
  4. * @projectDescription lock a table header in place while scrolling - without breaking styles or events bound to the header
  5. *
  6. * Dependencies:
  7. * jquery 1.9.0 + [required] OR jquery 1.7.0 + jquery UI core
  8. *
  9. * http://mkoryak.github.io/floatThead/
  10. *
  11. * Tested on FF13+, Chrome 21+, IE8, IE9, IE10, IE11
  12. *
  13. */
  14. (function( $ ) {
  15. /**
  16. * provides a default config object. You can modify this after including this script if you want to change the init defaults
  17. * @type {Object}
  18. */
  19. $.floatThead = $.floatThead || {};
  20. $.floatThead.defaults = {
  21. headerCellSelector: 'tr:visible:first>*:visible', //thead cells are this.
  22. zIndex: 1001, //zindex of the floating thead (actually a container div)
  23. position: 'auto', // 'fixed', 'absolute', 'auto'. auto picks the best for your table scrolling type.
  24. top: 0, //String or function($table) - offset from top of window where the header should not pass above
  25. bottom: 0, //String or function($table) - offset from the bottom of the table where the header should stop scrolling
  26. 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
  27. return $([]);
  28. },
  29. 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
  30. return $([]);
  31. },
  32. getSizingRow: function($table, $cols, $fthCells){ // this is only called when using IE,
  33. // override it if the first row of the table is going to contain colgroups (any cell spans greater than one col)
  34. // it should return a jquery object containing a wrapped set of table cells comprising a row that contains no col spans and is visible
  35. return $table.find('tbody tr:visible:first>*:visible');
  36. },
  37. floatTableClass: 'floatThead-table',
  38. floatWrapperClass: 'floatThead-wrapper',
  39. floatContainerClass: 'floatThead-container',
  40. copyTableClass: true, //copy 'class' attribute from table into the floated table so that the styles match.
  41. enableAria: false, //will copy header text from the floated header back into the table for screen readers. Might cause the css styling to be off. beware!
  42. autoReflow: false, //(undocumented) - use MutationObserver api to reflow automatically when internal table DOM changes
  43. debug: false //print possible issues (that don't prevent script loading) to console, if console exists.
  44. };
  45. var util = window._;
  46. var canObserveMutations = typeof MutationObserver !== 'undefined';
  47. //browser stuff
  48. var ieVersion = function(){for(var a=3,b=document.createElement("b"),c=b.all||[];a = 1+a,b.innerHTML="<!--[if gt IE "+ a +"]><i><![endif]-->",c[0];);return 4<a?a:document.documentMode}();
  49. var isFF = /Gecko\//.test(navigator.userAgent);
  50. var isWebkit = /WebKit\//.test(navigator.userAgent);
  51. if(!(ieVersion || isFF || isWebkit)){
  52. ieVersion = 11; //yey a hack!
  53. }
  54. //safari 7 (and perhaps others) reports table width to be parent container's width if max-width is set on table. see: https://github.com/mkoryak/floatThead/issues/108
  55. var isTableWidthBug = function(){
  56. if(isWebkit) {
  57. var $test = $('<div style="width:0px"><table style="max-width:100%"><tr><th><div style="min-width:100px;">X</div></th></tr></table></div>');
  58. $("body").append($test);
  59. var ret = ($test.find("table").width() == 0);
  60. $test.remove();
  61. return ret;
  62. }
  63. return false;
  64. };
  65. var createElements = !isFF && !ieVersion; //FF can read width from <col> elements, but webkit cannot
  66. var $window = $(window);
  67. if(!window.matchMedia) {
  68. //these will be used by the plugin to go into print mode (destroy and remake itself)
  69. var _beforePrint = window.onbeforeprint;
  70. var _afterPrint = window.onafterprint;
  71. window.onbeforeprint = function () {
  72. _beforePrint && _beforePrint();
  73. $window.triggerHandler("beforeprint");
  74. };
  75. window.onafterprint = function () {
  76. _afterPrint && _afterPrint();
  77. $window.triggerHandler("afterprint");
  78. };
  79. }
  80. /**
  81. * @param debounceMs
  82. * @param cb
  83. */
  84. function windowResize(eventName, cb){
  85. if(ieVersion == 8){ //ie8 is crap: https://github.com/mkoryak/floatThead/issues/65
  86. var winWidth = $window.width();
  87. var debouncedCb = util.debounce(function(){
  88. var winWidthNew = $window.width();
  89. if(winWidth != winWidthNew){
  90. winWidth = winWidthNew;
  91. cb();
  92. }
  93. }, 1);
  94. $window.bind(eventName, debouncedCb);
  95. } else {
  96. $window.bind(eventName, util.debounce(cb, 1));
  97. }
  98. }
  99. function getTrueOffsetParent($elem) {
  100. var elem = $elem[0];
  101. var parent = elem.offsetParent;
  102. if (!parent) {
  103. parent = elem.parentElement;
  104. do {
  105. var pos = window
  106. .getComputedStyle(parent)
  107. .getPropertyValue('position');
  108. if (pos != 'static') break;
  109. if (parent.offsetParent) {
  110. parent = parent.offsetParent;
  111. break;
  112. }
  113. } while (parent = parent.parentElement)
  114. }
  115. if(parent == document.body){
  116. return $([]);
  117. }
  118. return $(parent);
  119. }
  120. function debug(str){
  121. window && window.console && window.console.error && window.console.error("jQuery.floatThead: " + str);
  122. }
  123. //returns fractional pixel widths
  124. function getOffsetWidth(el) {
  125. var rect = el.getBoundingClientRect();
  126. return rect.width || rect.right - rect.left;
  127. }
  128. /**
  129. * try to calculate the scrollbar width for your browser/os
  130. * @return {Number}
  131. */
  132. function scrollbarWidth() {
  133. var $div = $( //borrowed from anti-scroll
  134. '<div style="width:50px;height:50px;overflow-y:scroll;'
  135. + 'position:absolute;top:-200px;left:-200px;"><div style="height:100px;width:100%">'
  136. + '</div>'
  137. );
  138. $('body').append($div);
  139. var w1 = $div.innerWidth();
  140. var w2 = $('div', $div).innerWidth();
  141. $div.remove();
  142. return w1 - w2;
  143. }
  144. /**
  145. * Check if a given table has been datatableized (http://datatables.net)
  146. * @param $table
  147. * @return {Boolean}
  148. */
  149. function isDatatable($table){
  150. if($table.dataTableSettings){
  151. for(var i = 0; i < $table.dataTableSettings.length; i++){
  152. var table = $table.dataTableSettings[i].nTable;
  153. if($table[0] == table){
  154. return true;
  155. }
  156. }
  157. }
  158. return false;
  159. }
  160. function tableWidth($table, $fthCells, isOuter){
  161. // see: https://github.com/mkoryak/floatThead/issues/108
  162. var fn = isOuter ? "outerWidth": "width";
  163. if(isTableWidthBug && $table.css("max-width")){
  164. var w = 0;
  165. if(isOuter) {
  166. w += parseInt($table.css("borderLeft"), 10);
  167. w += parseInt($table.css("borderRight"), 10);
  168. }
  169. for(var i=0; i < $fthCells.length; i++){
  170. w += $fthCells.get(i).offsetWidth;
  171. }
  172. return w;
  173. } else {
  174. return $table[fn]();
  175. }
  176. }
  177. $.fn.floatThead = function(map){
  178. map = map || {};
  179. if(!util){ //may have been included after the script? lets try to grab it again.
  180. util = window._ || $.floatThead._;
  181. if(!util){
  182. throw new Error("jquery.floatThead-slim.js requires underscore. You should use the non-lite version since you do not have underscore.");
  183. }
  184. }
  185. if(ieVersion < 8){
  186. return this; //no more crappy browser support.
  187. }
  188. var mObs = null; //mutation observer lives in here if we can use it / make it
  189. if(util.isFunction(isTableWidthBug)) {
  190. isTableWidthBug = isTableWidthBug();
  191. }
  192. if(util.isString(map)){
  193. var command = map;
  194. var ret = this;
  195. this.filter('table').each(function(){
  196. var $this = $(this);
  197. var opts = $this.data('floatThead-lazy');
  198. if(opts){
  199. $this.floatThead(opts);
  200. }
  201. var obj = $this.data('floatThead-attached');
  202. if(obj && util.isFunction(obj[command])){
  203. var r = obj[command]();
  204. if(typeof r !== 'undefined'){
  205. ret = r;
  206. }
  207. }
  208. });
  209. return ret;
  210. }
  211. var opts = $.extend({}, $.floatThead.defaults || {}, map);
  212. $.each(map, function(key, val){
  213. if((!(key in $.floatThead.defaults)) && opts.debug){
  214. 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(', '));
  215. }
  216. });
  217. if(opts.debug){
  218. var v = $.fn.jquery.split(".");
  219. if(parseInt(v[0], 10) == 1 && parseInt(v[1], 10) <= 7){
  220. 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")
  221. }
  222. }
  223. this.filter(':not(.'+opts.floatTableClass+')').each(function(){
  224. var floatTheadId = util.uniqueId();
  225. var $table = $(this);
  226. if($table.data('floatThead-attached')){
  227. return true; //continue the each loop
  228. }
  229. if(!$table.is('table')){
  230. throw new Error('jQuery.floatThead must be run on a table element. ex: $("table").floatThead();');
  231. }
  232. canObserveMutations = opts.autoReflow && canObserveMutations; //option defaults to false!
  233. var $header = $table.children('thead:first');
  234. var $tbody = $table.children('tbody:first');
  235. if($header.length == 0 || $tbody.length == 0){
  236. $table.data('floatThead-lazy', opts);
  237. $table.unbind("reflow").one('reflow', function(){
  238. $table.floatThead(opts);
  239. });
  240. return;
  241. }
  242. if($table.data('floatThead-lazy')){
  243. $table.unbind("reflow");
  244. }
  245. $table.data('floatThead-lazy', false);
  246. var headerFloated = true;
  247. var scrollingTop, scrollingBottom;
  248. var scrollbarOffset = {vertical: 0, horizontal: 0};
  249. var scWidth = scrollbarWidth();
  250. var lastColumnCount = 0; //used by columnNum()
  251. if(opts.scrollContainer === true){
  252. opts.scrollContainer = getTrueOffsetParent;
  253. }
  254. var $scrollContainer = opts.scrollContainer($table) || $([]); //guard against returned nulls
  255. var locked = $scrollContainer.length > 0;
  256. var $responsiveContainer = locked ? $([]) : opts.responsiveContainer($table) || $([]);
  257. var responsive = isResponsiveContainerActive();
  258. var useAbsolutePositioning = null;
  259. if(typeof opts.useAbsolutePositioning !== 'undefined'){
  260. opts.position = 'auto';
  261. if(opts.useAbsolutePositioning){
  262. opts.position = opts.useAbsolutePositioning ? 'absolute' : 'fixed';
  263. }
  264. debug("option 'useAbsolutePositioning' has been removed in v1.3.0, use `position:'"+opts.position+"'` instead. See docs for more info: http://mkoryak.github.io/floatThead/#options")
  265. }
  266. if(typeof opts.scrollingTop !== 'undefined'){
  267. opts.top = opts.scrollingTop;
  268. debug("option 'scrollingTop' has been renamed to 'top' in v1.3.0. See docs for more info: http://mkoryak.github.io/floatThead/#options");
  269. }
  270. if(typeof opts.scrollingBottom !== 'undefined'){
  271. opts.bottom = opts.scrollingBottom;
  272. debug("option 'scrollingBottom' has been renamed to 'bottom' in v1.3.0. See docs for more info: http://mkoryak.github.io/floatThead/#options");
  273. }
  274. if (opts.position == 'auto') {
  275. useAbsolutePositioning = null;
  276. } else if (opts.position == 'fixed') {
  277. useAbsolutePositioning = false;
  278. } else if (opts.position == 'absolute'){
  279. useAbsolutePositioning = true;
  280. } else if (opts.debug) {
  281. debug('Invalid value given to "position" option, valid is "fixed", "absolute" and "auto". You passed: ', opts.position);
  282. }
  283. if(useAbsolutePositioning == null){ //defaults: locked=true, !locked=false
  284. useAbsolutePositioning = locked;
  285. }
  286. var $caption = $table.find("caption");
  287. var haveCaption = $caption.length == 1;
  288. if(haveCaption){
  289. var captionAlignTop = ($caption.css("caption-side") || $caption.attr("align") || "top") === "top";
  290. }
  291. var $fthGrp = $('<fthfoot style="display:table-footer-group;border-spacing:0;height:0;border-collapse:collapse;visibility:hidden"/>');
  292. var wrappedContainer = false; //used with absolute positioning enabled. did we need to wrap the scrollContainer/table with a relative div?
  293. var $wrapper = $([]); //used when absolute positioning enabled - wraps the table and the float container
  294. 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.
  295. var $floatTable = $("<table/>");
  296. var $floatColGroup = $("<colgroup/>");
  297. var $tableColGroup = $table.children('colgroup:first');
  298. var existingColGroup = true;
  299. if($tableColGroup.length == 0){
  300. $tableColGroup = $("<colgroup/>");
  301. existingColGroup = false;
  302. }
  303. var $fthRow = $('<fthtr style="display:table-row;border-spacing:0;height:0;border-collapse:collapse"/>'); //created unstyled elements (used for sizing the table because chrome can't read <col> width)
  304. var $floatContainer = $('<div style="overflow: hidden;" aria-hidden="true"></div>');
  305. var floatTableHidden = false; //this happens when the table is hidden and we do magic when making it visible
  306. var $newHeader = $("<thead/>");
  307. var $sizerRow = $('<tr class="size-row"/>');
  308. var $sizerCells = $([]);
  309. 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.
  310. var $headerCells = $([]);
  311. var $fthCells = $([]); //created elements
  312. $newHeader.append($sizerRow);
  313. $table.prepend($tableColGroup);
  314. if(createElements){
  315. $fthGrp.append($fthRow);
  316. $table.append($fthGrp);
  317. }
  318. $floatTable.append($floatColGroup);
  319. $floatContainer.append($floatTable);
  320. if(opts.copyTableClass){
  321. $floatTable.attr('class', $table.attr('class'));
  322. }
  323. $floatTable.attr({ //copy over some deprecated table attributes that people still like to use. Good thing people don't use colgroups...
  324. 'cellpadding': $table.attr('cellpadding'),
  325. 'cellspacing': $table.attr('cellspacing'),
  326. 'border': $table.attr('border')
  327. });
  328. var tableDisplayCss = $table.css('display');
  329. $floatTable.css({
  330. 'borderCollapse': $table.css('borderCollapse'),
  331. 'border': $table.css('border'),
  332. 'display': tableDisplayCss
  333. });
  334. if(tableDisplayCss == 'none'){
  335. floatTableHidden = true;
  336. }
  337. $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
  338. if(useAbsolutePositioning){
  339. var makeRelative = function($container, alwaysWrap){
  340. var positionCss = $container.css('position');
  341. var relativeToScrollContainer = (positionCss == "relative" || positionCss == "absolute");
  342. var $containerWrap = $container;
  343. if(!relativeToScrollContainer || alwaysWrap){
  344. var css = {"paddingLeft": $container.css('paddingLeft'), "paddingRight": $container.css('paddingRight')};
  345. $floatContainer.css(css);
  346. $containerWrap = $container.data('floatThead-containerWrap') || $container.wrap("<div class='"+opts.floatWrapperClass+"' style='position: relative; clear:both;'></div>").parent();
  347. $container.data('floatThead-containerWrap', $containerWrap); //multiple tables inside one scrolling container - #242
  348. wrappedContainer = true;
  349. }
  350. return $containerWrap;
  351. };
  352. if(locked){
  353. $wrapper = makeRelative($scrollContainer, true);
  354. $wrapper.prepend($floatContainer);
  355. } else {
  356. $wrapper = makeRelative($table);
  357. $table.before($floatContainer);
  358. }
  359. } else {
  360. $table.before($floatContainer);
  361. }
  362. $floatContainer.css({
  363. position: useAbsolutePositioning ? 'absolute' : 'fixed',
  364. marginTop: 0,
  365. top: useAbsolutePositioning ? 0 : 'auto',
  366. zIndex: opts.zIndex
  367. });
  368. $floatContainer.addClass(opts.floatContainerClass);
  369. updateScrollingOffsets();
  370. var layoutFixed = {'table-layout': 'fixed'};
  371. var layoutAuto = {'table-layout': $table.css('tableLayout') || 'auto'};
  372. var originalTableWidth = $table[0].style.width || ""; //setting this to auto is bad: #70
  373. var originalTableMinWidth = $table.css('minWidth') || "";
  374. function eventName(name){
  375. return name+'.fth-'+floatTheadId+'.floatTHead'
  376. }
  377. function setHeaderHeight(){
  378. var headerHeight = 0;
  379. $header.children("tr:visible").each(function(){
  380. headerHeight += $(this).outerHeight(true);
  381. });
  382. if($table.css('border-collapse') == 'collapse') {
  383. var tableBorderTopHeight = parseInt($table.css('border-top-width'), 10);
  384. var cellBorderTopHeight = parseInt($table.find("thead tr:first").find(">*:first").css('border-top-width'), 10);
  385. if(tableBorderTopHeight > cellBorderTopHeight) {
  386. headerHeight -= (tableBorderTopHeight / 2); //id love to see some docs where this magic recipe is found..
  387. }
  388. }
  389. $sizerRow.outerHeight(headerHeight);
  390. $sizerCells.outerHeight(headerHeight);
  391. }
  392. function setFloatWidth(){
  393. var tw = tableWidth($table, $fthCells, true);
  394. var $container = responsive ? $responsiveContainer : $scrollContainer;
  395. var width = $container.width() || tw;
  396. var floatContainerWidth = $container.css("overflow-y") != 'hidden' ? width - scrollbarOffset.vertical : width;
  397. $floatContainer.width(floatContainerWidth);
  398. if(locked){
  399. var percent = 100 * tw / (floatContainerWidth);
  400. $floatTable.css('width', percent+'%');
  401. } else {
  402. $floatTable.outerWidth(tw);
  403. }
  404. }
  405. function updateScrollingOffsets(){
  406. scrollingTop = (util.isFunction(opts.top) ? opts.top($table) : opts.top) || 0;
  407. scrollingBottom = (util.isFunction(opts.bottom) ? opts.bottom($table) : opts.bottom) || 0;
  408. }
  409. /**
  410. * get the number of columns and also rebuild resizer rows if the count is different than the last count
  411. */
  412. function columnNum(){
  413. var count;
  414. var $headerColumns = $header.find(opts.headerCellSelector);
  415. if(existingColGroup){
  416. count = $tableColGroup.find('col').length;
  417. } else {
  418. count = 0;
  419. $headerColumns.each(function () {
  420. count += parseInt(($(this).attr('colspan') || 1), 10);
  421. });
  422. }
  423. if(count != lastColumnCount){
  424. lastColumnCount = count;
  425. var cells = [], cols = [], psuedo = [], content;
  426. for(var x = 0; x < count; x++){
  427. if (opts.enableAria && (content = $headerColumns.eq(x).text()) ) {
  428. cells.push('<th scope="col" class="floatThead-col">' + content + '</th>');
  429. } else {
  430. cells.push('<th class="floatThead-col"/>');
  431. }
  432. cols.push('<col/>');
  433. psuedo.push("<fthtd style='display:table-cell;height:0;width:auto;'/>");
  434. }
  435. cols = cols.join('');
  436. cells = cells.join('');
  437. if(createElements){
  438. psuedo = psuedo.join('');
  439. $fthRow.html(psuedo);
  440. $fthCells = $fthRow.find('fthtd');
  441. }
  442. $sizerRow.html(cells);
  443. $sizerCells = $sizerRow.find("th");
  444. if(!existingColGroup){
  445. $tableColGroup.html(cols);
  446. }
  447. $tableCells = $tableColGroup.find('col');
  448. $floatColGroup.html(cols);
  449. $headerCells = $floatColGroup.find("col");
  450. }
  451. return count;
  452. }
  453. function refloat(){ //make the thing float
  454. if(!headerFloated){
  455. headerFloated = true;
  456. if(useAbsolutePositioning){ //#53, #56
  457. var tw = tableWidth($table, $fthCells, true);
  458. var wrapperWidth = $wrapper.width();
  459. if(tw > wrapperWidth){
  460. $table.css('minWidth', tw);
  461. }
  462. }
  463. $table.css(layoutFixed);
  464. $floatTable.css(layoutFixed);
  465. $floatTable.append($header); //append because colgroup must go first in chrome
  466. $tbody.before($newHeader);
  467. setHeaderHeight();
  468. }
  469. }
  470. function unfloat(){ //put the header back into the table
  471. if(headerFloated){
  472. headerFloated = false;
  473. if(useAbsolutePositioning){ //#53, #56
  474. $table.width(originalTableWidth);
  475. }
  476. $newHeader.detach();
  477. $table.prepend($header);
  478. $table.css(layoutAuto);
  479. $floatTable.css(layoutAuto);
  480. $table.css('minWidth', originalTableMinWidth); //this looks weird, but it's not a bug. Think about it!!
  481. $table.css('minWidth', tableWidth($table, $fthCells)); //#121
  482. }
  483. }
  484. var isHeaderFloatingLogical = false; //for the purpose of this event, the header is/isnt floating, even though the element
  485. //might be in some other state. this is what the header looks like to the user
  486. function triggerFloatEvent(isFloating){
  487. if(isHeaderFloatingLogical != isFloating){
  488. isHeaderFloatingLogical = isFloating;
  489. $table.triggerHandler("floatThead", [isFloating, $floatContainer])
  490. }
  491. }
  492. function changePositioning(isAbsolute){
  493. if(useAbsolutePositioning != isAbsolute){
  494. useAbsolutePositioning = isAbsolute;
  495. $floatContainer.css({
  496. position: useAbsolutePositioning ? 'absolute' : 'fixed'
  497. });
  498. }
  499. }
  500. function getSizingRow($table, $cols, $fthCells, ieVersion){
  501. if(createElements){
  502. return $fthCells;
  503. } else if(ieVersion) {
  504. return opts.getSizingRow($table, $cols, $fthCells);
  505. } else {
  506. return $cols;
  507. }
  508. }
  509. /**
  510. * returns a function that updates the floating header's cell widths.
  511. * @return {Function}
  512. */
  513. function reflow(){
  514. var i;
  515. var numCols = columnNum(); //if the tables columns changed dynamically since last time (datatables), rebuild the sizer rows and get a new count
  516. return function(){
  517. $tableCells = $tableColGroup.find('col');
  518. var $rowCells = getSizingRow($table, $tableCells, $fthCells, ieVersion);
  519. if($rowCells.length == numCols && numCols > 0){
  520. if(!existingColGroup){
  521. for(i=0; i < numCols; i++){
  522. $tableCells.eq(i).css('width', '');
  523. }
  524. }
  525. unfloat();
  526. var widths = [];
  527. for(i=0; i < numCols; i++){
  528. widths[i] = getOffsetWidth($rowCells.get(i));
  529. }
  530. for(i=0; i < numCols; i++){
  531. $headerCells.eq(i).width(widths[i]);
  532. $tableCells.eq(i).width(widths[i]);
  533. }
  534. refloat();
  535. } else {
  536. $floatTable.append($header);
  537. $table.css(layoutAuto);
  538. $floatTable.css(layoutAuto);
  539. setHeaderHeight();
  540. }
  541. $table.triggerHandler("reflowed", [$floatContainer]);
  542. };
  543. }
  544. function floatContainerBorderWidth(side){
  545. var border = $scrollContainer.css("border-"+side+"-width");
  546. var w = 0;
  547. if (border && ~border.indexOf('px')) {
  548. w = parseInt(border, 10);
  549. }
  550. return w;
  551. }
  552. function isResponsiveContainerActive(){
  553. return $responsiveContainer.css("overflow-x") == 'auto';
  554. }
  555. /**
  556. * first performs initial calculations that we expect to not change when the table, window, or scrolling container are scrolled.
  557. * 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
  558. * @return {Function}
  559. */
  560. function calculateFloatContainerPosFn(){
  561. var scrollingContainerTop = $scrollContainer.scrollTop();
  562. //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)
  563. var floatEnd;
  564. var tableContainerGap = 0;
  565. var captionHeight = haveCaption ? $caption.outerHeight(true) : 0;
  566. var captionScrollOffset = captionAlignTop ? captionHeight : -captionHeight;
  567. var floatContainerHeight = $floatContainer.height();
  568. var tableOffset = $table.offset();
  569. var tableLeftGap = 0; //can be caused by border on container (only in locked mode)
  570. var tableTopGap = 0;
  571. if(locked){
  572. var containerOffset = $scrollContainer.offset();
  573. tableContainerGap = tableOffset.top - containerOffset.top + scrollingContainerTop;
  574. if(haveCaption && captionAlignTop){
  575. tableContainerGap += captionHeight;
  576. }
  577. tableLeftGap = floatContainerBorderWidth('left');
  578. tableTopGap = floatContainerBorderWidth('top');
  579. tableContainerGap -= tableTopGap;
  580. } else {
  581. floatEnd = tableOffset.top - scrollingTop - floatContainerHeight + scrollingBottom + scrollbarOffset.horizontal;
  582. }
  583. var windowTop = $window.scrollTop();
  584. var windowLeft = $window.scrollLeft();
  585. var scrollContainerLeft = (isResponsiveContainerActive() ? $responsiveContainer : $scrollContainer).scrollLeft();
  586. return function(eventType){
  587. responsive = isResponsiveContainerActive();
  588. var isTableHidden = $table[0].offsetWidth <= 0 && $table[0].offsetHeight <= 0;
  589. if(!isTableHidden && floatTableHidden) {
  590. floatTableHidden = false;
  591. setTimeout(function(){
  592. $table.triggerHandler("reflow");
  593. }, 1);
  594. return null;
  595. }
  596. if(isTableHidden){ //it's hidden
  597. floatTableHidden = true;
  598. if(!useAbsolutePositioning){
  599. return null;
  600. }
  601. }
  602. if(eventType == 'windowScroll'){
  603. windowTop = $window.scrollTop();
  604. windowLeft = $window.scrollLeft();
  605. } else if(eventType == 'containerScroll'){
  606. if($responsiveContainer.length){
  607. if(!responsive){
  608. return; //we dont care about the event if we arent responsive right now
  609. }
  610. scrollContainerLeft = $responsiveContainer.scrollLeft();
  611. } else {
  612. scrollingContainerTop = $scrollContainer.scrollTop();
  613. scrollContainerLeft = $scrollContainer.scrollLeft();
  614. }
  615. } else if(eventType != 'init') {
  616. windowTop = $window.scrollTop();
  617. windowLeft = $window.scrollLeft();
  618. scrollingContainerTop = $scrollContainer.scrollTop();
  619. scrollContainerLeft = (responsive ? $responsiveContainer : $scrollContainer).scrollLeft();
  620. }
  621. if(isWebkit && (windowTop < 0 || windowLeft < 0)){ //chrome overscroll effect at the top of the page - breaks fixed positioned floated headers
  622. return;
  623. }
  624. if(absoluteToFixedOnScroll){
  625. if(eventType == 'windowScrollDone'){
  626. changePositioning(true); //change to absolute
  627. } else {
  628. changePositioning(false); //change to fixed
  629. }
  630. } else if(eventType == 'windowScrollDone'){
  631. return null; //event is fired when they stop scrolling. ignore it if not 'absoluteToFixedOnScroll'
  632. }
  633. tableOffset = $table.offset();
  634. if(haveCaption && captionAlignTop){
  635. tableOffset.top += captionHeight;
  636. }
  637. var top, left;
  638. var tableHeight = $table.outerHeight();
  639. if(locked && useAbsolutePositioning){ //inner scrolling, absolute positioning
  640. if (tableContainerGap >= scrollingContainerTop) {
  641. var gap = tableContainerGap - scrollingContainerTop + tableTopGap;
  642. top = gap > 0 ? gap : 0;
  643. triggerFloatEvent(false);
  644. } else {
  645. top = wrappedContainer ? tableTopGap : scrollingContainerTop;
  646. //headers stop at the top of the viewport
  647. triggerFloatEvent(true);
  648. }
  649. left = tableLeftGap;
  650. } else if(!locked && useAbsolutePositioning) { //window scrolling, absolute positioning
  651. if(windowTop > floatEnd + tableHeight + captionScrollOffset){
  652. top = tableHeight - floatContainerHeight + captionScrollOffset; //scrolled past table
  653. } else if (tableOffset.top >= windowTop + scrollingTop) {
  654. top = 0; //scrolling to table
  655. unfloat();
  656. triggerFloatEvent(false);
  657. } else {
  658. top = scrollingTop + windowTop - tableOffset.top + tableContainerGap + (captionAlignTop ? captionHeight : 0);
  659. refloat(); //scrolling within table. header floated
  660. triggerFloatEvent(true);
  661. }
  662. left = scrollContainerLeft;
  663. } else if(locked && !useAbsolutePositioning){ //inner scrolling, fixed positioning
  664. if (tableContainerGap > scrollingContainerTop || scrollingContainerTop - tableContainerGap > tableHeight) {
  665. top = tableOffset.top - windowTop;
  666. unfloat();
  667. triggerFloatEvent(false);
  668. } else {
  669. top = tableOffset.top + scrollingContainerTop - windowTop - tableContainerGap;
  670. refloat();
  671. triggerFloatEvent(true);
  672. //headers stop at the top of the viewport
  673. }
  674. left = tableOffset.left + scrollContainerLeft - windowLeft;
  675. } else if(!locked && !useAbsolutePositioning) { //window scrolling, fixed positioning
  676. if(windowTop > floatEnd + tableHeight + captionScrollOffset){
  677. top = tableHeight + scrollingTop - windowTop + floatEnd + captionScrollOffset;
  678. //scrolled past the bottom of the table
  679. } else if (tableOffset.top > windowTop + scrollingTop) {
  680. top = tableOffset.top - windowTop;
  681. refloat();
  682. triggerFloatEvent(false); //this is a weird case, the header never gets unfloated and i have no no way to know
  683. //scrolled past the top of the table
  684. } else {
  685. //scrolling within the table
  686. top = scrollingTop;
  687. triggerFloatEvent(true);
  688. }
  689. left = tableOffset.left + scrollContainerLeft - windowLeft;
  690. }
  691. return {top: top, left: left};
  692. };
  693. }
  694. /**
  695. * returns a function that caches old floating container position and only updates css when the position changes
  696. * @return {Function}
  697. */
  698. function repositionFloatContainerFn(){
  699. var oldTop = null;
  700. var oldLeft = null;
  701. var oldScrollLeft = null;
  702. return function(pos, setWidth, setHeight){
  703. if(pos != null && (oldTop != pos.top || oldLeft != pos.left)){
  704. $floatContainer.css({
  705. top: pos.top,
  706. left: pos.left
  707. });
  708. oldTop = pos.top;
  709. oldLeft = pos.left;
  710. }
  711. if(setWidth){
  712. setFloatWidth();
  713. }
  714. if(setHeight){
  715. setHeaderHeight();
  716. }
  717. var scrollLeft = (responsive ? $responsiveContainer : $scrollContainer).scrollLeft();
  718. if(!useAbsolutePositioning || oldScrollLeft != scrollLeft){
  719. $floatContainer.scrollLeft(scrollLeft);
  720. oldScrollLeft = scrollLeft;
  721. }
  722. }
  723. }
  724. /**
  725. * checks if THIS table has scrollbars, and finds their widths
  726. */
  727. function calculateScrollBarSize(){ //this should happen after the floating table has been positioned
  728. if($scrollContainer.length){
  729. if($scrollContainer.data().perfectScrollbar){
  730. scrollbarOffset = {horizontal:0, vertical:0};
  731. } else {
  732. var sw = $scrollContainer.width(), sh = $scrollContainer.height(), th = $table.height(), tw = tableWidth($table, $fthCells);
  733. var offseth = sw < tw ? scWidth : 0;
  734. var offsetv = sh < th ? scWidth : 0;
  735. scrollbarOffset.horizontal = sw - offsetv < tw ? scWidth : 0;
  736. scrollbarOffset.vertical = sh - offseth < th ? scWidth : 0;
  737. }
  738. }
  739. }
  740. //finish up. create all calculation functions and bind them to events
  741. calculateScrollBarSize();
  742. var flow;
  743. var ensureReflow = function(){
  744. flow = reflow();
  745. flow();
  746. };
  747. ensureReflow();
  748. var calculateFloatContainerPos = calculateFloatContainerPosFn();
  749. var repositionFloatContainer = repositionFloatContainerFn();
  750. repositionFloatContainer(calculateFloatContainerPos('init'), true); //this must come after reflow because reflow changes scrollLeft back to 0 when it rips out the thead
  751. var windowScrollDoneEvent = util.debounce(function(){
  752. repositionFloatContainer(calculateFloatContainerPos('windowScrollDone'), false);
  753. }, 1);
  754. var windowScrollEvent = function(){
  755. repositionFloatContainer(calculateFloatContainerPos('windowScroll'), false);
  756. if(absoluteToFixedOnScroll){
  757. windowScrollDoneEvent();
  758. }
  759. };
  760. var containerScrollEvent = function(){
  761. repositionFloatContainer(calculateFloatContainerPos('containerScroll'), false);
  762. };
  763. var windowResizeEvent = function(){
  764. if($table.is(":hidden")){
  765. return;
  766. }
  767. updateScrollingOffsets();
  768. calculateScrollBarSize();
  769. ensureReflow();
  770. calculateFloatContainerPos = calculateFloatContainerPosFn();
  771. repositionFloatContainer = repositionFloatContainerFn();
  772. repositionFloatContainer(calculateFloatContainerPos('resize'), true, true);
  773. };
  774. var reflowEvent = util.debounce(function(){
  775. if($table.is(":hidden")){
  776. return;
  777. }
  778. calculateScrollBarSize();
  779. updateScrollingOffsets();
  780. ensureReflow();
  781. calculateFloatContainerPos = calculateFloatContainerPosFn();
  782. repositionFloatContainer(calculateFloatContainerPos('reflow'), true);
  783. }, 1);
  784. /////// printing stuff
  785. var beforePrint = function(){
  786. $table.floatThead('destroy', [true]);
  787. };
  788. var afterPrint = function(){
  789. $table.floatThead(opts);
  790. };
  791. var printEvent = function(mql){
  792. //make printing the table work properly on IE10+
  793. if(mql.matches) {
  794. beforePrint();
  795. } else {
  796. afterPrint();
  797. }
  798. };
  799. if(window.matchMedia){
  800. window.matchMedia("print").addListener(printEvent);
  801. } else {
  802. $window.bind('beforeprint', beforePrint);
  803. $window.bind('afterprint', afterPrint);
  804. }
  805. ////// end printing stuff
  806. if(locked){ //internal scrolling
  807. if(useAbsolutePositioning){
  808. $scrollContainer.bind(eventName('scroll'), containerScrollEvent);
  809. } else {
  810. $scrollContainer.bind(eventName('scroll'), containerScrollEvent);
  811. $window.bind(eventName('scroll'), windowScrollEvent);
  812. }
  813. } else { //window scrolling
  814. $responsiveContainer.bind(eventName('scroll'), containerScrollEvent);
  815. $window.bind(eventName('scroll'), windowScrollEvent);
  816. }
  817. $window.bind(eventName('load'), reflowEvent); //for tables with images
  818. windowResize(eventName('resize'), windowResizeEvent);
  819. $table.bind('reflow', reflowEvent);
  820. if(isDatatable($table)){
  821. $table
  822. .bind('filter', reflowEvent)
  823. .bind('sort', reflowEvent)
  824. .bind('page', reflowEvent);
  825. }
  826. $window.bind(eventName('shown.bs.tab'), reflowEvent); // people cant seem to figure out how to use this plugin with bs3 tabs... so this :P
  827. $window.bind(eventName('tabsactivate'), reflowEvent); // same thing for jqueryui
  828. if (canObserveMutations) {
  829. var mutationElement = null;
  830. if(util.isFunction(opts.autoReflow)){
  831. mutationElement = opts.autoReflow($table, $scrollContainer)
  832. }
  833. if(!mutationElement) {
  834. mutationElement = $scrollContainer.length ? $scrollContainer[0] : $table[0]
  835. }
  836. mObs = new MutationObserver(function(e){
  837. var wasTableRelated = function(nodes){
  838. return nodes && nodes[0] && (nodes[0].nodeName == "THEAD" || nodes[0].nodeName == "TD"|| nodes[0].nodeName == "TH");
  839. };
  840. for(var i=0; i < e.length; i++){
  841. if(!(wasTableRelated(e[i].addedNodes) || wasTableRelated(e[i].removedNodes))){
  842. reflowEvent();
  843. break;
  844. }
  845. }
  846. });
  847. mObs.observe(mutationElement, {
  848. childList: true,
  849. subtree: true
  850. });
  851. }
  852. //attach some useful functions to the table.
  853. $table.data('floatThead-attached', {
  854. destroy: function(e, isPrintEvent){
  855. var ns = '.fth-'+floatTheadId;
  856. unfloat();
  857. $table.css(layoutAuto);
  858. $tableColGroup.remove();
  859. createElements && $fthGrp.remove();
  860. if($newHeader.parent().length){ //only if it's in the DOM
  861. $newHeader.replaceWith($header);
  862. }
  863. triggerFloatEvent(false);
  864. if(canObserveMutations){
  865. mObs.disconnect();
  866. mObs = null;
  867. }
  868. $table.unbind('reflow reflowed');
  869. $scrollContainer.unbind(ns);
  870. $responsiveContainer.unbind(ns);
  871. if (wrappedContainer) {
  872. if ($scrollContainer.length) {
  873. $scrollContainer.unwrap();
  874. }
  875. else {
  876. $table.unwrap();
  877. }
  878. }
  879. if(locked){
  880. $scrollContainer.data('floatThead-containerWrap', false);
  881. } else {
  882. $table.data('floatThead-containerWrap', false);
  883. }
  884. $table.css('minWidth', originalTableMinWidth);
  885. $floatContainer.remove();
  886. $table.data('floatThead-attached', false);
  887. $window.unbind(ns);
  888. if(!isPrintEvent){
  889. //if we are in the middle of printing, we want this event to re-create the plugin
  890. window.matchMedia && window.matchMedia("print").removeListener(printEvent);
  891. beforePrint = afterPrint = function(){};
  892. }
  893. },
  894. reflow: function(){
  895. reflowEvent();
  896. },
  897. setHeaderHeight: function(){
  898. setHeaderHeight();
  899. },
  900. getFloatContainer: function(){
  901. return $floatContainer;
  902. },
  903. getRowGroups: function(){
  904. if(headerFloated){
  905. return $floatContainer.find('>table>thead').add($table.children("tbody,tfoot"));
  906. } else {
  907. return $table.children("thead,tbody,tfoot");
  908. }
  909. }
  910. });
  911. });
  912. return this;
  913. };
  914. })(jQuery);
  915. /* jQuery.floatThead.utils - http://mkoryak.github.io/floatThead/ - Copyright (c) 2012 - 2014 Misha Koryak
  916. * License: MIT
  917. *
  918. * This file is required if you do not use underscore in your project and you want to use floatThead.
  919. * It contains functions from underscore that the plugin uses.
  920. *
  921. * YOU DON'T NEED TO INCLUDE THIS IF YOU ALREADY INCLUDE UNDERSCORE!
  922. *
  923. */
  924. (function($){
  925. $.floatThead = $.floatThead || {};
  926. $.floatThead._ = window._ || (function(){
  927. var that = {};
  928. var hasOwnProperty = Object.prototype.hasOwnProperty, isThings = ['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'];
  929. that.has = function(obj, key) {
  930. return hasOwnProperty.call(obj, key);
  931. };
  932. that.keys = function(obj) {
  933. if (obj !== Object(obj)) throw new TypeError('Invalid object');
  934. var keys = [];
  935. for (var key in obj) if (that.has(obj, key)) keys.push(key);
  936. return keys;
  937. };
  938. var idCounter = 0;
  939. that.uniqueId = function(prefix) {
  940. var id = ++idCounter + '';
  941. return prefix ? prefix + id : id;
  942. };
  943. $.each(isThings, function(){
  944. var name = this;
  945. that['is' + name] = function(obj) {
  946. return Object.prototype.toString.call(obj) == '[object ' + name + ']';
  947. };
  948. });
  949. that.debounce = function(func, wait, immediate) {
  950. var timeout, args, context, timestamp, result;
  951. return function() {
  952. context = this;
  953. args = arguments;
  954. timestamp = new Date();
  955. var later = function() {
  956. var last = (new Date()) - timestamp;
  957. if (last < wait) {
  958. timeout = setTimeout(later, wait - last);
  959. } else {
  960. timeout = null;
  961. if (!immediate) result = func.apply(context, args);
  962. }
  963. };
  964. var callNow = immediate && !timeout;
  965. if (!timeout) {
  966. timeout = setTimeout(later, wait);
  967. }
  968. if (callNow) result = func.apply(context, args);
  969. return result;
  970. };
  971. };
  972. return that;
  973. })();
  974. })(jQuery);