1/*! FixedHeader 3.4.0 2 * © SpryMedia Ltd - datatables.net/license 3 */ 4 5(function( factory ){ 6 if ( typeof define === 'function' && define.amd ) { 7 // AMD 8 define( ['jquery', 'datatables.net'], function ( $ ) { 9 return factory( $, window, document ); 10 } ); 11 } 12 else if ( typeof exports === 'object' ) { 13 // CommonJS 14 var jq = require('jquery'); 15 var cjsRequires = function (root, $) { 16 if ( ! $.fn.dataTable ) { 17 require('datatables.net')(root, $); 18 } 19 }; 20 21 if (typeof window === 'undefined') { 22 module.exports = function (root, $) { 23 if ( ! root ) { 24 // CommonJS environments without a window global must pass a 25 // root. This will give an error otherwise 26 root = window; 27 } 28 29 if ( ! $ ) { 30 $ = jq( root ); 31 } 32 33 cjsRequires( root, $ ); 34 return factory( $, root, root.document ); 35 }; 36 } 37 else { 38 cjsRequires( window, jq ); 39 module.exports = factory( jq, window, window.document ); 40 } 41 } 42 else { 43 // Browser 44 factory( jQuery, window, document ); 45 } 46}(function( $, window, document, undefined ) { 47'use strict'; 48var DataTable = $.fn.dataTable; 49 50 51 52/** 53 * @summary FixedHeader 54 * @description Fix a table's header or footer, so it is always visible while 55 * scrolling 56 * @version 3.4.0 57 * @author SpryMedia Ltd (www.sprymedia.co.uk) 58 * @contact www.sprymedia.co.uk 59 * @copyright SpryMedia Ltd. 60 * 61 * This source file is free software, available under the following license: 62 * MIT license - http://datatables.net/license/mit 63 * 64 * This source file is distributed in the hope that it will be useful, but 65 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 66 * or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details. 67 * 68 * For details please refer to: http://www.datatables.net 69 */ 70 71var _instCounter = 0; 72 73var FixedHeader = function (dt, config) { 74 // Sanity check - you just know it will happen 75 if (!(this instanceof FixedHeader)) { 76 throw "FixedHeader must be initialised with the 'new' keyword."; 77 } 78 79 // Allow a boolean true for defaults 80 if (config === true) { 81 config = {}; 82 } 83 84 dt = new DataTable.Api(dt); 85 86 this.c = $.extend(true, {}, FixedHeader.defaults, config); 87 88 this.s = { 89 dt: dt, 90 position: { 91 theadTop: 0, 92 tbodyTop: 0, 93 tfootTop: 0, 94 tfootBottom: 0, 95 width: 0, 96 left: 0, 97 tfootHeight: 0, 98 theadHeight: 0, 99 windowHeight: $(window).height(), 100 visible: true 101 }, 102 headerMode: null, 103 footerMode: null, 104 autoWidth: dt.settings()[0].oFeatures.bAutoWidth, 105 namespace: '.dtfc' + _instCounter++, 106 scrollLeft: { 107 header: -1, 108 footer: -1 109 }, 110 enable: true, 111 autoDisable: false 112 }; 113 114 this.dom = { 115 floatingHeader: null, 116 thead: $(dt.table().header()), 117 tbody: $(dt.table().body()), 118 tfoot: $(dt.table().footer()), 119 header: { 120 host: null, 121 floating: null, 122 floatingParent: $('<div class="dtfh-floatingparent">'), 123 placeholder: null 124 }, 125 footer: { 126 host: null, 127 floating: null, 128 floatingParent: $('<div class="dtfh-floatingparent">'), 129 placeholder: null 130 } 131 }; 132 133 this.dom.header.host = this.dom.thead.parent(); 134 this.dom.footer.host = this.dom.tfoot.parent(); 135 136 var dtSettings = dt.settings()[0]; 137 if (dtSettings._fixedHeader) { 138 throw 'FixedHeader already initialised on table ' + dtSettings.nTable.id; 139 } 140 141 dtSettings._fixedHeader = this; 142 143 this._constructor(); 144}; 145 146/* 147 * Variable: FixedHeader 148 * Purpose: Prototype for FixedHeader 149 * Scope: global 150 */ 151$.extend(FixedHeader.prototype, { 152 /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 153 * API methods 154 */ 155 156 /** 157 * Kill off FH and any events 158 */ 159 destroy: function () { 160 var dom = this.dom; 161 162 this.s.dt.off('.dtfc'); 163 $(window).off(this.s.namespace); 164 165 // Remove clones of FC blockers 166 if (dom.header.rightBlocker) { 167 dom.header.rightBlocker.remove(); 168 } 169 if (dom.header.leftBlocker) { 170 dom.header.leftBlocker.remove(); 171 } 172 if (dom.footer.rightBlocker) { 173 dom.footer.rightBlocker.remove(); 174 } 175 if (dom.footer.leftBlocker) { 176 dom.footer.leftBlocker.remove(); 177 } 178 179 if (this.c.header) { 180 this._modeChange('in-place', 'header', true); 181 } 182 183 if (this.c.footer && dom.tfoot.length) { 184 this._modeChange('in-place', 'footer', true); 185 } 186 }, 187 188 /** 189 * Enable / disable the fixed elements 190 * 191 * @param {boolean} enable `true` to enable, `false` to disable 192 */ 193 enable: function (enable, update, type) { 194 this.s.enable = enable; 195 196 this.s.enableType = type; 197 198 if (update || update === undefined) { 199 this._positions(); 200 this._scroll(true); 201 } 202 }, 203 204 /** 205 * Get enabled status 206 */ 207 enabled: function () { 208 return this.s.enable; 209 }, 210 211 /** 212 * Set header offset 213 * 214 * @param {int} new value for headerOffset 215 */ 216 headerOffset: function (offset) { 217 if (offset !== undefined) { 218 this.c.headerOffset = offset; 219 this.update(); 220 } 221 222 return this.c.headerOffset; 223 }, 224 225 /** 226 * Set footer offset 227 * 228 * @param {int} new value for footerOffset 229 */ 230 footerOffset: function (offset) { 231 if (offset !== undefined) { 232 this.c.footerOffset = offset; 233 this.update(); 234 } 235 236 return this.c.footerOffset; 237 }, 238 239 /** 240 * Recalculate the position of the fixed elements and force them into place 241 */ 242 update: function (force) { 243 var table = this.s.dt.table().node(); 244 245 // Update should only do something if enabled by the dev. 246 if (!this.s.enable && !this.s.autoDisable) { 247 return; 248 } 249 250 if ($(table).is(':visible')) { 251 this.s.autoDisable = false; 252 this.enable(true, false); 253 } 254 else { 255 this.s.autoDisable = true; 256 this.enable(false, false); 257 } 258 259 // Don't update if header is not in the document atm (due to 260 // async events) 261 if ($(table).children('thead').length === 0) { 262 return; 263 } 264 265 this._positions(); 266 this._scroll(force !== undefined ? force : true); 267 }, 268 269 /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 270 * Constructor 271 */ 272 273 /** 274 * FixedHeader constructor - adding the required event listeners and 275 * simple initialisation 276 * 277 * @private 278 */ 279 _constructor: function () { 280 var that = this; 281 var dt = this.s.dt; 282 283 $(window) 284 .on('scroll' + this.s.namespace, function () { 285 that._scroll(); 286 }) 287 .on( 288 'resize' + this.s.namespace, 289 DataTable.util.throttle(function () { 290 that.s.position.windowHeight = $(window).height(); 291 that.update(); 292 }, 50) 293 ); 294 295 var autoHeader = $('.fh-fixedHeader'); 296 if (!this.c.headerOffset && autoHeader.length) { 297 this.c.headerOffset = autoHeader.outerHeight(); 298 } 299 300 var autoFooter = $('.fh-fixedFooter'); 301 if (!this.c.footerOffset && autoFooter.length) { 302 this.c.footerOffset = autoFooter.outerHeight(); 303 } 304 305 dt.on( 306 'column-reorder.dt.dtfc column-visibility.dt.dtfc column-sizing.dt.dtfc responsive-display.dt.dtfc', 307 function (e, ctx) { 308 that.update(); 309 } 310 ).on('draw.dt.dtfc', function (e, ctx) { 311 // For updates from our own table, don't reclone, but for all others, do 312 that.update(ctx === dt.settings()[0] ? false : true); 313 }); 314 315 dt.on('destroy.dtfc', function () { 316 that.destroy(); 317 }); 318 319 this._positions(); 320 this._scroll(); 321 }, 322 323 /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 324 * Private methods 325 */ 326 327 /** 328 * Clone a fixed item to act as a place holder for the original element 329 * which is moved into a clone of the table element, and moved around the 330 * document to give the fixed effect. 331 * 332 * @param {string} item 'header' or 'footer' 333 * @param {boolean} force Force the clone to happen, or allow automatic 334 * decision (reuse existing if available) 335 * @private 336 */ 337 _clone: function (item, force) { 338 var that = this; 339 var dt = this.s.dt; 340 var itemDom = this.dom[item]; 341 var itemElement = item === 'header' ? this.dom.thead : this.dom.tfoot; 342 343 // If footer and scrolling is enabled then we don't clone 344 // Instead the table's height is decreased accordingly - see `_scroll()` 345 if (item === 'footer' && this._scrollEnabled()) { 346 return; 347 } 348 349 if (!force && itemDom.floating) { 350 // existing floating element - reuse it 351 itemDom.floating.removeClass('fixedHeader-floating fixedHeader-locked'); 352 } 353 else { 354 if (itemDom.floating) { 355 if (itemDom.placeholder !== null) { 356 itemDom.placeholder.remove(); 357 } 358 this._unsize(item); 359 itemDom.floating.children().detach(); 360 itemDom.floating.remove(); 361 } 362 363 var tableNode = $(dt.table().node()); 364 var scrollBody = $(tableNode.parent()); 365 var scrollEnabled = this._scrollEnabled(); 366 367 itemDom.floating = $(dt.table().node().cloneNode(false)) 368 .attr('aria-hidden', 'true') 369 .css({ 370 'table-layout': 'fixed', 371 top: 0, 372 left: 0 373 }) 374 .removeAttr('id') 375 .append(itemElement); 376 377 itemDom.floatingParent 378 .css({ 379 width: scrollBody.width(), 380 overflow: 'hidden', 381 height: 'fit-content', 382 position: 'fixed', 383 left: scrollEnabled ? tableNode.offset().left + scrollBody.scrollLeft() : 0 384 }) 385 .css( 386 item === 'header' 387 ? { 388 top: this.c.headerOffset, 389 bottom: '' 390 } 391 : { 392 top: '', 393 bottom: this.c.footerOffset 394 } 395 ) 396 .addClass(item === 'footer' ? 'dtfh-floatingparentfoot' : 'dtfh-floatingparenthead') 397 .append(itemDom.floating) 398 .appendTo('body'); 399 400 this._stickyPosition(itemDom.floating, '-'); 401 402 var scrollLeftUpdate = function () { 403 var scrollLeft = scrollBody.scrollLeft(); 404 that.s.scrollLeft = { footer: scrollLeft, header: scrollLeft }; 405 itemDom.floatingParent.scrollLeft(that.s.scrollLeft.header); 406 }; 407 408 scrollLeftUpdate(); 409 scrollBody.off('scroll.dtfh').on('scroll.dtfh', scrollLeftUpdate); 410 411 // Insert a fake thead/tfoot into the DataTable to stop it jumping around 412 itemDom.placeholder = itemElement.clone(false); 413 itemDom.placeholder.find('*[id]').removeAttr('id'); 414 415 itemDom.host.prepend(itemDom.placeholder); 416 417 // Clone widths 418 this._matchWidths(itemDom.placeholder, itemDom.floating); 419 } 420 }, 421 422 /** 423 * This method sets the sticky position of the header elements to match fixed columns 424 * @param {JQuery<HTMLElement>} el 425 * @param {string} sign 426 */ 427 _stickyPosition: function (el, sign) { 428 if (this._scrollEnabled()) { 429 var that = this; 430 var rtl = $(that.s.dt.table().node()).css('direction') === 'rtl'; 431 432 el.find('th').each(function () { 433 // Find out if fixed header has previously set this column 434 if ($(this).css('position') === 'sticky') { 435 var right = $(this).css('right'); 436 var left = $(this).css('left'); 437 if (right !== 'auto' && !rtl) { 438 // New position either adds or dismisses the barWidth 439 var potential = 440 +right.replace(/px/g, '') + 441 (sign === '-' ? -1 : 1) * that.s.dt.settings()[0].oBrowser.barWidth; 442 $(this).css('right', potential > 0 ? potential : 0); 443 } 444 else if (left !== 'auto' && rtl) { 445 var potential = 446 +left.replace(/px/g, '') + 447 (sign === '-' ? -1 : 1) * that.s.dt.settings()[0].oBrowser.barWidth; 448 $(this).css('left', potential > 0 ? potential : 0); 449 } 450 } 451 }); 452 } 453 }, 454 455 /** 456 * Copy widths from the cells in one element to another. This is required 457 * for the footer as the footer in the main table takes its sizes from the 458 * header columns. That isn't present in the footer so to have it still 459 * align correctly, the sizes need to be copied over. It is also required 460 * for the header when auto width is not enabled 461 * 462 * @param {jQuery} from Copy widths from 463 * @param {jQuery} to Copy widths to 464 * @private 465 */ 466 _matchWidths: function (from, to) { 467 var get = function (name) { 468 return $(name, from) 469 .map(function () { 470 return ( 471 $(this) 472 .css('width') 473 .replace(/[^\d\.]/g, '') * 1 474 ); 475 }) 476 .toArray(); 477 }; 478 479 var set = function (name, toWidths) { 480 $(name, to).each(function (i) { 481 $(this).css({ 482 width: toWidths[i], 483 minWidth: toWidths[i] 484 }); 485 }); 486 }; 487 488 var thWidths = get('th'); 489 var tdWidths = get('td'); 490 491 set('th', thWidths); 492 set('td', tdWidths); 493 }, 494 495 /** 496 * Remove assigned widths from the cells in an element. This is required 497 * when inserting the footer back into the main table so the size is defined 498 * by the header columns and also when auto width is disabled in the 499 * DataTable. 500 * 501 * @param {string} item The `header` or `footer` 502 * @private 503 */ 504 _unsize: function (item) { 505 var el = this.dom[item].floating; 506 507 if (el && (item === 'footer' || (item === 'header' && !this.s.autoWidth))) { 508 $('th, td', el).css({ 509 width: '', 510 minWidth: '' 511 }); 512 } 513 else if (el && item === 'header') { 514 $('th, td', el).css('min-width', ''); 515 } 516 }, 517 518 /** 519 * Reposition the floating elements to take account of horizontal page 520 * scroll 521 * 522 * @param {string} item The `header` or `footer` 523 * @param {int} scrollLeft Document scrollLeft 524 * @private 525 */ 526 _horizontal: function (item, scrollLeft) { 527 var itemDom = this.dom[item]; 528 var position = this.s.position; 529 var lastScrollLeft = this.s.scrollLeft; 530 531 if (itemDom.floating && lastScrollLeft[item] !== scrollLeft) { 532 // If scrolling is enabled we need to match the floating header to the body 533 if (this._scrollEnabled()) { 534 var newScrollLeft = $($(this.s.dt.table().node()).parent()).scrollLeft(); 535 itemDom.floating.scrollLeft(newScrollLeft); 536 itemDom.floatingParent.scrollLeft(newScrollLeft); 537 } 538 539 lastScrollLeft[item] = scrollLeft; 540 } 541 }, 542 543 /** 544 * Change from one display mode to another. Each fixed item can be in one 545 * of: 546 * 547 * * `in-place` - In the main DataTable 548 * * `in` - Floating over the DataTable 549 * * `below` - (Header only) Fixed to the bottom of the table body 550 * * `above` - (Footer only) Fixed to the top of the table body 551 * 552 * @param {string} mode Mode that the item should be shown in 553 * @param {string} item 'header' or 'footer' 554 * @param {boolean} forceChange Force a redraw of the mode, even if already 555 * in that mode. 556 * @private 557 */ 558 _modeChange: function (mode, item, forceChange) { 559 var dt = this.s.dt; 560 var itemDom = this.dom[item]; 561 var position = this.s.position; 562 563 // Just determine if scroll is enabled once 564 var scrollEnabled = this._scrollEnabled(); 565 566 // If footer and scrolling is enabled then we don't clone 567 // Instead the table's height is decreased accordingly - see `_scroll()` 568 if (item === 'footer' && scrollEnabled) { 569 return; 570 } 571 572 // It isn't trivial to add a !important css attribute... 573 var importantWidth = function (w) { 574 itemDom.floating.attr('style', function (i, s) { 575 return (s || '') + 'width: ' + w + 'px !important;'; 576 }); 577 578 // If not scrolling also have to update the floatingParent 579 if (!scrollEnabled) { 580 itemDom.floatingParent.attr('style', function (i, s) { 581 return (s || '') + 'width: ' + w + 'px !important;'; 582 }); 583 } 584 }; 585 586 // Record focus. Browser's will cause input elements to loose focus if 587 // they are inserted else where in the doc 588 var tablePart = this.dom[item === 'footer' ? 'tfoot' : 'thead']; 589 var focus = $.contains(tablePart[0], document.activeElement) 590 ? document.activeElement 591 : null; 592 var scrollBody = $($(this.s.dt.table().node()).parent()); 593 594 if (mode === 'in-place') { 595 // Insert the header back into the table's real header 596 if (itemDom.placeholder) { 597 itemDom.placeholder.remove(); 598 itemDom.placeholder = null; 599 } 600 601 this._unsize(item); 602 603 if (item === 'header') { 604 itemDom.host.prepend(tablePart); 605 } 606 else { 607 itemDom.host.append(tablePart); 608 } 609 610 if (itemDom.floating) { 611 itemDom.floating.remove(); 612 itemDom.floating = null; 613 this._stickyPosition(itemDom.host, '+'); 614 } 615 616 if (itemDom.floatingParent) { 617 itemDom.floatingParent.remove(); 618 } 619 620 $($(itemDom.host.parent()).parent()).scrollLeft(scrollBody.scrollLeft()); 621 } 622 else if (mode === 'in') { 623 // Remove the header from the read header and insert into a fixed 624 // positioned floating table clone 625 this._clone(item, forceChange); 626 627 // Get useful position values 628 var scrollOffset = scrollBody.offset(); 629 var windowTop = $(document).scrollTop(); 630 var windowHeight = $(window).height(); 631 var windowBottom = windowTop + windowHeight; 632 var bodyTop = scrollEnabled ? scrollOffset.top : position.tbodyTop; 633 var bodyBottom = scrollEnabled 634 ? scrollOffset.top + scrollBody.outerHeight() 635 : position.tfootTop; 636 637 // Calculate the amount that the footer or header needs to be shuffled 638 var shuffle = 639 item === 'footer' 640 ? // footer and top of body isn't on screen 641 bodyTop > windowBottom 642 ? // Yes - push the footer below 643 position.tfootHeight 644 : // No - bottom set to the gap between the top of the body and the bottom of the window 645 bodyTop + position.tfootHeight - windowBottom 646 : // Otherwise must be a header so get the difference from the bottom of the 647 // desired floating header and the bottom of the table body 648 windowTop + this.c.headerOffset + position.theadHeight - bodyBottom; 649 650 // Set the top or bottom based off of the offset and the shuffle value 651 var prop = item === 'header' ? 'top' : 'bottom'; 652 var val = this.c[item + 'Offset'] - (shuffle > 0 ? shuffle : 0); 653 654 itemDom.floating.addClass('fixedHeader-floating'); 655 itemDom.floatingParent 656 .css(prop, val) 657 .css({ 658 left: position.left, 659 height: item === 'header' ? position.theadHeight : position.tfootHeight, 660 'z-index': 2 661 }) 662 .append(itemDom.floating); 663 664 importantWidth(position.width); 665 666 if (item === 'footer') { 667 itemDom.floating.css('top', ''); 668 } 669 } 670 else if (mode === 'below') { 671 // only used for the header 672 // Fix the position of the floating header at base of the table body 673 this._clone(item, forceChange); 674 675 itemDom.floating.addClass('fixedHeader-locked'); 676 itemDom.floatingParent.css({ 677 position: 'absolute', 678 top: position.tfootTop - position.theadHeight, 679 left: position.left + 'px' 680 }); 681 682 importantWidth(position.width); 683 } 684 else if (mode === 'above') { 685 // only used for the footer 686 // Fix the position of the floating footer at top of the table body 687 this._clone(item, forceChange); 688 689 itemDom.floating.addClass('fixedHeader-locked'); 690 itemDom.floatingParent.css({ 691 position: 'absolute', 692 top: position.tbodyTop, 693 left: position.left + 'px' 694 }); 695 696 importantWidth(position.width); 697 } 698 699 // Restore focus if it was lost 700 if (focus && focus !== document.activeElement) { 701 setTimeout(function () { 702 focus.focus(); 703 }, 10); 704 } 705 706 this.s.scrollLeft.header = -1; 707 this.s.scrollLeft.footer = -1; 708 this.s[item + 'Mode'] = mode; 709 }, 710 711 /** 712 * Cache the positional information that is required for the mode 713 * calculations that FixedHeader performs. 714 * 715 * @private 716 */ 717 _positions: function () { 718 var dt = this.s.dt; 719 var table = dt.table(); 720 var position = this.s.position; 721 var dom = this.dom; 722 var tableNode = $(table.node()); 723 var scrollEnabled = this._scrollEnabled(); 724 725 // Need to use the header and footer that are in the main table, 726 // regardless of if they are clones, since they hold the positions we 727 // want to measure from 728 var thead = $(dt.table().header()); 729 var tfoot = $(dt.table().footer()); 730 var tbody = dom.tbody; 731 var scrollBody = tableNode.parent(); 732 733 position.visible = tableNode.is(':visible'); 734 position.width = tableNode.outerWidth(); 735 position.left = tableNode.offset().left; 736 position.theadTop = thead.offset().top; 737 position.tbodyTop = scrollEnabled ? scrollBody.offset().top : tbody.offset().top; 738 position.tbodyHeight = scrollEnabled ? scrollBody.outerHeight() : tbody.outerHeight(); 739 position.theadHeight = thead.outerHeight(); 740 position.theadBottom = position.theadTop + position.theadHeight; 741 742 if (tfoot.length) { 743 position.tfootTop = position.tbodyTop + position.tbodyHeight; //tfoot.offset().top; 744 position.tfootBottom = position.tfootTop + tfoot.outerHeight(); 745 position.tfootHeight = tfoot.outerHeight(); 746 } 747 else { 748 position.tfootTop = position.tbodyTop + tbody.outerHeight(); 749 position.tfootBottom = position.tfootTop; 750 position.tfootHeight = position.tfootTop; 751 } 752 }, 753 754 /** 755 * Mode calculation - determine what mode the fixed items should be placed 756 * into. 757 * 758 * @param {boolean} forceChange Force a redraw of the mode, even if already 759 * in that mode. 760 * @private 761 */ 762 _scroll: function (forceChange) { 763 if (this.s.dt.settings()[0].bDestroying) { 764 return; 765 } 766 767 // ScrollBody details 768 var scrollEnabled = this._scrollEnabled(); 769 var scrollBody = $(this.s.dt.table().node()).parent(); 770 var scrollOffset = scrollBody.offset(); 771 var scrollHeight = scrollBody.outerHeight(); 772 773 // Window details 774 var windowLeft = $(document).scrollLeft(); 775 var windowTop = $(document).scrollTop(); 776 var windowHeight = $(window).height(); 777 var windowBottom = windowHeight + windowTop; 778 779 var position = this.s.position; 780 var headerMode, footerMode; 781 782 // Body Details 783 var bodyTop = scrollEnabled ? scrollOffset.top : position.tbodyTop; 784 var bodyLeft = scrollEnabled ? scrollOffset.left : position.left; 785 var bodyBottom = scrollEnabled ? scrollOffset.top + scrollHeight : position.tfootTop; 786 var bodyWidth = scrollEnabled ? scrollBody.outerWidth() : position.tbodyWidth; 787 788 var windowBottom = windowTop + windowHeight; 789 790 if (this.c.header) { 791 if (!this.s.enable) { 792 headerMode = 'in-place'; 793 } 794 // The header is in it's normal place if the body top is lower than 795 // the scroll of the window plus the headerOffset and the height of the header 796 else if ( 797 !position.visible || 798 windowTop + this.c.headerOffset + position.theadHeight <= bodyTop 799 ) { 800 headerMode = 'in-place'; 801 } 802 // The header should be floated if 803 else if ( 804 // The scrolling plus the header offset plus the height of the header is lower than the top of the body 805 windowTop + this.c.headerOffset + position.theadHeight > bodyTop && 806 // And the scrolling at the top plus the header offset is above the bottom of the body 807 windowTop + this.c.headerOffset + position.theadHeight < bodyBottom 808 ) { 809 headerMode = 'in'; 810 var scrollBody = $($(this.s.dt.table().node()).parent()); 811 812 // Further to the above, If the scrolling plus the header offset plus the header height is lower 813 // than the bottom of the table a shuffle is required so have to force the calculation 814 if ( 815 windowTop + this.c.headerOffset + position.theadHeight > bodyBottom || 816 this.dom.header.floatingParent === undefined 817 ) { 818 forceChange = true; 819 } 820 else { 821 this.dom.header.floatingParent 822 .css({ 823 top: this.c.headerOffset, 824 position: 'fixed' 825 }) 826 .append(this.dom.header.floating); 827 } 828 } 829 // Anything else and the view is below the table 830 else { 831 headerMode = 'below'; 832 } 833 834 if (forceChange || headerMode !== this.s.headerMode) { 835 this._modeChange(headerMode, 'header', forceChange); 836 } 837 838 this._horizontal('header', windowLeft); 839 } 840 841 var header = { 842 offset: { top: 0, left: 0 }, 843 height: 0 844 }; 845 var footer = { 846 offset: { top: 0, left: 0 }, 847 height: 0 848 }; 849 850 if (this.c.footer && this.dom.tfoot.length) { 851 if (!this.s.enable) { 852 footerMode = 'in-place'; 853 } 854 else if ( 855 !position.visible || 856 position.tfootBottom + this.c.footerOffset <= windowBottom 857 ) { 858 footerMode = 'in-place'; 859 } 860 else if ( 861 bodyBottom + position.tfootHeight + this.c.footerOffset > windowBottom && 862 bodyTop + this.c.footerOffset < windowBottom 863 ) { 864 footerMode = 'in'; 865 forceChange = true; 866 } 867 else { 868 footerMode = 'above'; 869 } 870 871 if (forceChange || footerMode !== this.s.footerMode) { 872 this._modeChange(footerMode, 'footer', forceChange); 873 } 874 875 this._horizontal('footer', windowLeft); 876 877 var getOffsetHeight = function (el) { 878 return { 879 offset: el.offset(), 880 height: el.outerHeight() 881 }; 882 }; 883 884 header = this.dom.header.floating 885 ? getOffsetHeight(this.dom.header.floating) 886 : getOffsetHeight(this.dom.thead); 887 footer = this.dom.footer.floating 888 ? getOffsetHeight(this.dom.footer.floating) 889 : getOffsetHeight(this.dom.tfoot); 890 891 // If scrolling is enabled and the footer is off the screen 892 if (scrollEnabled && footer.offset.top > windowTop) { 893 // && footer.offset.top >= windowBottom) { 894 // Calculate the gap between the top of the scrollBody and the top of the window 895 var overlap = windowTop - scrollOffset.top; 896 // The new height is the bottom of the window 897 var newHeight = 898 windowBottom + 899 // If the gap between the top of the scrollbody and the window is more than 900 // the height of the header then the top of the table is still visible so add that gap 901 // Doing this has effectively calculated the height from the top of the table to the bottom of the current page 902 (overlap > -header.height ? overlap : 0) - 903 // Take from that 904 // The top of the header plus 905 (header.offset.top + 906 // The header height if the standard header is present 907 (overlap < -header.height ? header.height : 0) + 908 // And the height of the footer 909 footer.height); 910 911 // Don't want a negative height 912 if (newHeight < 0) { 913 newHeight = 0; 914 } 915 916 // At the end of the above calculation the space between the header (top of the page if floating) 917 // and the point just above the footer should be the new value for the height of the table. 918 scrollBody.outerHeight(newHeight); 919 920 // Need some rounding here as sometimes very small decimal places are encountered 921 // If the actual height is bigger or equal to the height we just applied then the footer is "Floating" 922 if (Math.round(scrollBody.outerHeight()) >= Math.round(newHeight)) { 923 $(this.dom.tfoot.parent()).addClass('fixedHeader-floating'); 924 } 925 // Otherwise max-width has kicked in so it is not floating 926 else { 927 $(this.dom.tfoot.parent()).removeClass('fixedHeader-floating'); 928 } 929 } 930 } 931 932 if (this.dom.header.floating) { 933 this.dom.header.floatingParent.css('left', bodyLeft - windowLeft); 934 } 935 if (this.dom.footer.floating) { 936 this.dom.footer.floatingParent.css('left', bodyLeft - windowLeft); 937 } 938 939 // If fixed columns is being used on this table then the blockers need to be copied across 940 // Cloning these is cleaner than creating as our own as it will keep consistency with fixedColumns automatically 941 // ASSUMING that the class remains the same 942 if (this.s.dt.settings()[0]._fixedColumns !== undefined) { 943 var adjustBlocker = function (side, end, el) { 944 if (el === undefined) { 945 var blocker = $('div.dtfc-' + side + '-' + end + '-blocker'); 946 947 el = blocker.length === 0 ? null : blocker.clone().css('z-index', 1); 948 } 949 950 if (el !== null) { 951 if (headerMode === 'in' || headerMode === 'below') { 952 el.appendTo('body').css({ 953 top: end === 'top' ? header.offset.top : footer.offset.top, 954 left: side === 'right' ? bodyLeft + bodyWidth - el.width() : bodyLeft 955 }); 956 } 957 else { 958 el.detach(); 959 } 960 } 961 962 return el; 963 }; 964 965 // Adjust all blockers 966 this.dom.header.rightBlocker = adjustBlocker( 967 'right', 968 'top', 969 this.dom.header.rightBlocker 970 ); 971 this.dom.header.leftBlocker = adjustBlocker('left', 'top', this.dom.header.leftBlocker); 972 this.dom.footer.rightBlocker = adjustBlocker( 973 'right', 974 'bottom', 975 this.dom.footer.rightBlocker 976 ); 977 this.dom.footer.leftBlocker = adjustBlocker( 978 'left', 979 'bottom', 980 this.dom.footer.leftBlocker 981 ); 982 } 983 }, 984 985 /** 986 * Function to check if scrolling is enabled on the table or not 987 * @returns Boolean value indicating if scrolling on the table is enabled or not 988 */ 989 _scrollEnabled: function () { 990 var oScroll = this.s.dt.settings()[0].oScroll; 991 if (oScroll.sY !== '' || oScroll.sX !== '') { 992 return true; 993 } 994 return false; 995 } 996}); 997 998/** 999 * Version 1000 * @type {String} 1001 * @static 1002 */ 1003FixedHeader.version = '3.4.0'; 1004 1005/** 1006 * Defaults 1007 * @type {Object} 1008 * @static 1009 */ 1010FixedHeader.defaults = { 1011 header: true, 1012 footer: false, 1013 headerOffset: 0, 1014 footerOffset: 0 1015}; 1016 1017/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 1018 * DataTables interfaces 1019 */ 1020 1021// Attach for constructor access 1022$.fn.dataTable.FixedHeader = FixedHeader; 1023$.fn.DataTable.FixedHeader = FixedHeader; 1024 1025// DataTables creation - check if the FixedHeader option has been defined on the 1026// table and if so, initialise 1027$(document).on('init.dt.dtfh', function (e, settings, json) { 1028 if (e.namespace !== 'dt') { 1029 return; 1030 } 1031 1032 var init = settings.oInit.fixedHeader; 1033 var defaults = DataTable.defaults.fixedHeader; 1034 1035 if ((init || defaults) && !settings._fixedHeader) { 1036 var opts = $.extend({}, defaults, init); 1037 1038 if (init !== false) { 1039 new FixedHeader(settings, opts); 1040 } 1041 } 1042}); 1043 1044// DataTables API methods 1045DataTable.Api.register('fixedHeader()', function () {}); 1046 1047DataTable.Api.register('fixedHeader.adjust()', function () { 1048 return this.iterator('table', function (ctx) { 1049 var fh = ctx._fixedHeader; 1050 1051 if (fh) { 1052 fh.update(); 1053 } 1054 }); 1055}); 1056 1057DataTable.Api.register('fixedHeader.enable()', function (flag) { 1058 return this.iterator('table', function (ctx) { 1059 var fh = ctx._fixedHeader; 1060 1061 flag = flag !== undefined ? flag : true; 1062 if (fh && flag !== fh.enabled()) { 1063 fh.enable(flag); 1064 } 1065 }); 1066}); 1067 1068DataTable.Api.register('fixedHeader.enabled()', function () { 1069 if (this.context.length) { 1070 var fh = this.context[0]._fixedHeader; 1071 1072 if (fh) { 1073 return fh.enabled(); 1074 } 1075 } 1076 1077 return false; 1078}); 1079 1080DataTable.Api.register('fixedHeader.disable()', function () { 1081 return this.iterator('table', function (ctx) { 1082 var fh = ctx._fixedHeader; 1083 1084 if (fh && fh.enabled()) { 1085 fh.enable(false); 1086 } 1087 }); 1088}); 1089 1090$.each(['header', 'footer'], function (i, el) { 1091 DataTable.Api.register('fixedHeader.' + el + 'Offset()', function (offset) { 1092 var ctx = this.context; 1093 1094 if (offset === undefined) { 1095 return ctx.length && ctx[0]._fixedHeader 1096 ? ctx[0]._fixedHeader[el + 'Offset']() 1097 : undefined; 1098 } 1099 1100 return this.iterator('table', function (ctx) { 1101 var fh = ctx._fixedHeader; 1102 1103 if (fh) { 1104 fh[el + 'Offset'](offset); 1105 } 1106 }); 1107 }); 1108}); 1109 1110 1111return DataTable; 1112})); 1113