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