| 875 |
lars |
1 |
/*! FixedHeader 3.0.0
|
|
|
2 |
* ©2009-2015 SpryMedia Ltd - datatables.net/license
|
|
|
3 |
*/
|
|
|
4 |
|
|
|
5 |
/**
|
|
|
6 |
* @summary FixedHeader
|
|
|
7 |
* @description Fix a table's header or footer, so it is always visible while
|
|
|
8 |
* scrolling
|
|
|
9 |
* @version 3.0.0
|
|
|
10 |
* @file dataTables.fixedHeader.js
|
|
|
11 |
* @author SpryMedia Ltd (www.sprymedia.co.uk)
|
|
|
12 |
* @contact www.sprymedia.co.uk/contact
|
|
|
13 |
* @copyright Copyright 2009-2015 SpryMedia Ltd.
|
|
|
14 |
*
|
|
|
15 |
* This source file is free software, available under the following license:
|
|
|
16 |
* MIT license - http://datatables.net/license/mit
|
|
|
17 |
*
|
|
|
18 |
* This source file is distributed in the hope that it will be useful, but
|
|
|
19 |
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
|
20 |
* or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details.
|
|
|
21 |
*
|
|
|
22 |
* For details please refer to: http://www.datatables.net
|
|
|
23 |
*/
|
|
|
24 |
|
|
|
25 |
|
|
|
26 |
(function(window, document, undefined) {
|
|
|
27 |
|
|
|
28 |
|
|
|
29 |
var factory = function( $, DataTable ) {
|
|
|
30 |
"use strict";
|
|
|
31 |
|
|
|
32 |
var _instCounter = 0;
|
|
|
33 |
|
|
|
34 |
var FixedHeader = function ( dt, config ) {
|
|
|
35 |
// Sanity check - you just know it will happen
|
|
|
36 |
if ( ! (this instanceof FixedHeader) ) {
|
|
|
37 |
throw "FixedHeader must be initialised with the 'new' keyword.";
|
|
|
38 |
}
|
|
|
39 |
|
|
|
40 |
// Allow a boolean true for defaults
|
|
|
41 |
if ( config === true ) {
|
|
|
42 |
config = {};
|
|
|
43 |
}
|
|
|
44 |
|
|
|
45 |
dt = new DataTable.Api( dt );
|
|
|
46 |
|
|
|
47 |
this.c = $.extend( true, {}, FixedHeader.defaults, config );
|
|
|
48 |
|
|
|
49 |
this.s = {
|
|
|
50 |
dt: dt,
|
|
|
51 |
position: {
|
|
|
52 |
theadTop: 0,
|
|
|
53 |
tbodyTop: 0,
|
|
|
54 |
tfootTop: 0,
|
|
|
55 |
tfootBottom: 0,
|
|
|
56 |
width: 0,
|
|
|
57 |
left: 0,
|
|
|
58 |
tfootHeight: 0,
|
|
|
59 |
theadHeight: 0,
|
|
|
60 |
windowHeight: $(window).height(),
|
|
|
61 |
visible: true
|
|
|
62 |
},
|
|
|
63 |
headerMode: null,
|
|
|
64 |
footerMode: null,
|
|
|
65 |
namespace: '.dtfc'+(_instCounter++)
|
|
|
66 |
};
|
|
|
67 |
|
|
|
68 |
this.dom = {
|
|
|
69 |
floatingHeader: null,
|
|
|
70 |
thead: $(dt.table().header()),
|
|
|
71 |
tbody: $(dt.table().body()),
|
|
|
72 |
tfoot: $(dt.table().footer()),
|
|
|
73 |
header: {
|
|
|
74 |
host: null,
|
|
|
75 |
floating: null,
|
|
|
76 |
placeholder: null
|
|
|
77 |
},
|
|
|
78 |
footer: {
|
|
|
79 |
host: null,
|
|
|
80 |
floating: null,
|
|
|
81 |
placeholder: null
|
|
|
82 |
}
|
|
|
83 |
};
|
|
|
84 |
|
|
|
85 |
this.dom.header.host = this.dom.thead.parent();
|
|
|
86 |
this.dom.footer.host = this.dom.tfoot.parent();
|
|
|
87 |
|
|
|
88 |
var dtSettings = dt.settings()[0];
|
|
|
89 |
if ( dtSettings._fixedHeader ) {
|
|
|
90 |
throw "FixedHeader already initialised on table "+dtSettings.nTable.id;
|
|
|
91 |
}
|
|
|
92 |
|
|
|
93 |
dtSettings._fixedHeader = this;
|
|
|
94 |
|
|
|
95 |
this._constructor();
|
|
|
96 |
};
|
|
|
97 |
|
|
|
98 |
|
|
|
99 |
/*
|
|
|
100 |
* Variable: FixedHeader
|
|
|
101 |
* Purpose: Prototype for FixedHeader
|
|
|
102 |
* Scope: global
|
|
|
103 |
*/
|
|
|
104 |
FixedHeader.prototype = {
|
|
|
105 |
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
|
106 |
* API methods
|
|
|
107 |
*/
|
|
|
108 |
|
|
|
109 |
/**
|
|
|
110 |
* Recalculate the position of the fixed elements and force them into place
|
|
|
111 |
*/
|
|
|
112 |
update: function () {
|
|
|
113 |
this._positions();
|
|
|
114 |
this._scroll( true );
|
|
|
115 |
},
|
|
|
116 |
|
|
|
117 |
|
|
|
118 |
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
|
119 |
* Constructor
|
|
|
120 |
*/
|
|
|
121 |
|
|
|
122 |
/**
|
|
|
123 |
* FixedHeader constructor - adding the required event listeners and
|
|
|
124 |
* simple initialisation
|
|
|
125 |
*
|
|
|
126 |
* @private
|
|
|
127 |
*/
|
|
|
128 |
_constructor: function ()
|
|
|
129 |
{
|
|
|
130 |
var that = this;
|
|
|
131 |
var dt = this.s.dt;
|
|
|
132 |
|
|
|
133 |
$(window)
|
|
|
134 |
.on( 'scroll'+this.s.namespace, function () {
|
|
|
135 |
that._scroll();
|
|
|
136 |
} )
|
|
|
137 |
.on( 'resize'+this.s.namespace, function () {
|
|
|
138 |
that.s.position.windowHeight = $(window).height();
|
|
|
139 |
that._positions();
|
|
|
140 |
that._scroll( true );
|
|
|
141 |
} );
|
|
|
142 |
|
|
|
143 |
dt
|
|
|
144 |
.on( 'column-reorder.dt.dtfc column-visibility.dt.dtfc', function () {
|
|
|
145 |
that._positions();
|
|
|
146 |
that._scroll( true );
|
|
|
147 |
} )
|
|
|
148 |
.on( 'draw.dtfc', function () {
|
|
|
149 |
that._positions();
|
|
|
150 |
that._scroll();
|
|
|
151 |
} );
|
|
|
152 |
|
|
|
153 |
dt.on( 'destroy.dtfc', function () {
|
|
|
154 |
dt.off( '.dtfc' );
|
|
|
155 |
$(window).off( this.s.namespace );
|
|
|
156 |
} );
|
|
|
157 |
|
|
|
158 |
this._positions();
|
|
|
159 |
this._scroll();
|
|
|
160 |
},
|
|
|
161 |
|
|
|
162 |
|
|
|
163 |
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
|
164 |
* Private methods
|
|
|
165 |
*/
|
|
|
166 |
|
|
|
167 |
/**
|
|
|
168 |
* Clone a fixed item to act as a place holder for the original element
|
|
|
169 |
* which is moved into a clone of the table element, and moved around the
|
|
|
170 |
* document to give the fixed effect.
|
|
|
171 |
*
|
|
|
172 |
* @param {string} item 'header' or 'footer'
|
|
|
173 |
* @param {boolean} force Force the clone to happen, or allow automatic
|
|
|
174 |
* decision (reuse existing if available)
|
|
|
175 |
* @private
|
|
|
176 |
*/
|
|
|
177 |
_clone: function ( item, force )
|
|
|
178 |
{
|
|
|
179 |
var dt = this.s.dt;
|
|
|
180 |
var itemDom = this.dom[ item ];
|
|
|
181 |
var itemElement = item === 'header' ?
|
|
|
182 |
this.dom.thead :
|
|
|
183 |
this.dom.tfoot;
|
|
|
184 |
|
|
|
185 |
if ( ! force && itemDom.floating ) {
|
|
|
186 |
// existing floating element - reuse it
|
|
|
187 |
itemDom.floating.removeClass( 'fixedHeader-floating fixedHeader-locked' );
|
|
|
188 |
}
|
|
|
189 |
else {
|
|
|
190 |
if ( itemDom.floating ) {
|
|
|
191 |
itemDom.placeholder.remove();
|
|
|
192 |
itemDom.floating.children().detach();
|
|
|
193 |
itemDom.floating.remove();
|
|
|
194 |
}
|
|
|
195 |
|
|
|
196 |
itemDom.floating = $( dt.table().node().cloneNode( false ) )
|
|
|
197 |
.removeAttr( 'id' )
|
|
|
198 |
.append( itemElement )
|
|
|
199 |
.appendTo( 'body' );
|
|
|
200 |
|
|
|
201 |
// Insert a fake thead/tfoot into the DataTable to stop it jumping around
|
|
|
202 |
itemDom.placeholder = itemElement.clone( false );
|
|
|
203 |
itemDom.host.append( itemDom.placeholder );
|
|
|
204 |
|
|
|
205 |
// Footer needs sizes cloned across
|
|
|
206 |
if ( item === 'footer' ) {
|
|
|
207 |
this._footerMatch( itemDom.placeholder, itemDom.floating );
|
|
|
208 |
}
|
|
|
209 |
}
|
|
|
210 |
},
|
|
|
211 |
|
|
|
212 |
/**
|
|
|
213 |
* Copy widths from the cells in one element to another. This is required
|
|
|
214 |
* for the footer as the footer in the main table takes its sizes from the
|
|
|
215 |
* header columns. That isn't present in the footer so to have it still
|
|
|
216 |
* align correctly, the sizes need to be copied over.
|
|
|
217 |
*
|
|
|
218 |
* @param {jQuery} from Copy widths from
|
|
|
219 |
* @param {jQuery} to Copy widths to
|
|
|
220 |
* @private
|
|
|
221 |
*/
|
|
|
222 |
_footerMatch: function ( from, to ) {
|
|
|
223 |
var type = function ( name ) {
|
|
|
224 |
var toWidths = $(name, from)
|
|
|
225 |
.map( function () {
|
|
|
226 |
return $(this).width();
|
|
|
227 |
} ).toArray();
|
|
|
228 |
|
|
|
229 |
$(name, to).each( function ( i ) {
|
|
|
230 |
$(this).width( toWidths[i] );
|
|
|
231 |
} );
|
|
|
232 |
};
|
|
|
233 |
|
|
|
234 |
type( 'th' );
|
|
|
235 |
type( 'td' );
|
|
|
236 |
},
|
|
|
237 |
|
|
|
238 |
/**
|
|
|
239 |
* Remove assigned widths from the cells in an element. This is required
|
|
|
240 |
* when inserting the footer back into the main table so the size is defined
|
|
|
241 |
* by the header columns.
|
|
|
242 |
*
|
|
|
243 |
* @private
|
|
|
244 |
*/
|
|
|
245 |
_footerUnsize: function () {
|
|
|
246 |
var footer = this.dom.footer.floating;
|
|
|
247 |
|
|
|
248 |
if ( footer ) {
|
|
|
249 |
$('th, td', footer).css( 'width', '' );
|
|
|
250 |
}
|
|
|
251 |
},
|
|
|
252 |
|
|
|
253 |
/**
|
|
|
254 |
* Change from one display mode to another. Each fixed item can be in one
|
|
|
255 |
* of:
|
|
|
256 |
*
|
|
|
257 |
* * `in-place` - In the main DataTable
|
|
|
258 |
* * `in` - Floating over the DataTable
|
|
|
259 |
* * `below` - (Header only) Fixed to the bottom of the table body
|
|
|
260 |
* * `above` - (Footer only) Fixed to the top of the table body
|
|
|
261 |
*
|
|
|
262 |
* @param {string} mode Mode that the item should be shown in
|
|
|
263 |
* @param {string} item 'header' or 'footer'
|
|
|
264 |
* @param {boolean} forceChange Force a redraw of the mode, even if already
|
|
|
265 |
* in that mode.
|
|
|
266 |
* @private
|
|
|
267 |
*/
|
|
|
268 |
_modeChange: function ( mode, item, forceChange )
|
|
|
269 |
{
|
|
|
270 |
var dt = this.s.dt;
|
|
|
271 |
var itemDom = this.dom[ item ];
|
|
|
272 |
var position = this.s.position;
|
|
|
273 |
|
|
|
274 |
if ( mode === 'in-place' ) {
|
|
|
275 |
// Insert the header back into the table's real header
|
|
|
276 |
if ( itemDom.placeholder ) {
|
|
|
277 |
itemDom.placeholder.remove();
|
|
|
278 |
itemDom.placeholder = null;
|
|
|
279 |
}
|
|
|
280 |
|
|
|
281 |
itemDom.host.append( item === 'header' ?
|
|
|
282 |
this.dom.thead :
|
|
|
283 |
this.dom.tfoot
|
|
|
284 |
);
|
|
|
285 |
|
|
|
286 |
if ( itemDom.floating ) {
|
|
|
287 |
itemDom.floating.remove();
|
|
|
288 |
itemDom.floating = null;
|
|
|
289 |
}
|
|
|
290 |
|
|
|
291 |
if ( item === 'footer' ) {
|
|
|
292 |
this._footerUnsize();
|
|
|
293 |
}
|
|
|
294 |
}
|
|
|
295 |
else if ( mode === 'in' ) {
|
|
|
296 |
// Remove the header from the read header and insert into a fixed
|
|
|
297 |
// positioned floating table clone
|
|
|
298 |
this._clone( item, forceChange );
|
|
|
299 |
|
|
|
300 |
itemDom.floating
|
|
|
301 |
.addClass( 'fixedHeader-floating' )
|
|
|
302 |
.css( item === 'header' ? 'top' : 'bottom', this.c[item+'Offset'] )
|
|
|
303 |
.css( 'left', position.left+'px' )
|
|
|
304 |
.css( 'width', position.width+'px' );
|
|
|
305 |
|
|
|
306 |
if ( item === 'footer' ) {
|
|
|
307 |
itemDom.floating.css( 'top', '' );
|
|
|
308 |
}
|
|
|
309 |
}
|
|
|
310 |
else if ( mode === 'below' ) { // only used for the header
|
|
|
311 |
// Fix the position of the floating header at base of the table body
|
|
|
312 |
this._clone( item, forceChange );
|
|
|
313 |
|
|
|
314 |
itemDom.floating
|
|
|
315 |
.addClass( 'fixedHeader-locked' )
|
|
|
316 |
.css( 'top', position.tfootTop - position.theadHeight )
|
|
|
317 |
.css( 'left', position.left+'px' )
|
|
|
318 |
.css( 'width', position.width+'px' );
|
|
|
319 |
}
|
|
|
320 |
else if ( mode === 'above' ) { // only used for the footer
|
|
|
321 |
// Fix the position of the floating footer at top of the table body
|
|
|
322 |
this._clone( item, forceChange );
|
|
|
323 |
|
|
|
324 |
itemDom.floating
|
|
|
325 |
.addClass( 'fixedHeader-locked' )
|
|
|
326 |
.css( 'top', position.tbodyTop )
|
|
|
327 |
.css( 'left', position.left+'px' )
|
|
|
328 |
.css( 'width', position.width+'px' );
|
|
|
329 |
}
|
|
|
330 |
|
|
|
331 |
this.s[item+'Mode'] = mode;
|
|
|
332 |
},
|
|
|
333 |
|
|
|
334 |
/**
|
|
|
335 |
* Cache the positional information that is required for the mode
|
|
|
336 |
* calculations that FixedHeader performs.
|
|
|
337 |
*
|
|
|
338 |
* @private
|
|
|
339 |
*/
|
|
|
340 |
_positions: function ()
|
|
|
341 |
{
|
|
|
342 |
var dt = this.s.dt;
|
|
|
343 |
var table = dt.table();
|
|
|
344 |
var position = this.s.position;
|
|
|
345 |
var dom = this.dom;
|
|
|
346 |
var tableNode = $(table.node());
|
|
|
347 |
|
|
|
348 |
// Need to use the header and footer that are in the main table,
|
|
|
349 |
// regardless of if they are clones, since they hold the positions we
|
|
|
350 |
// want to measure from
|
|
|
351 |
var thead = tableNode.children('thead');
|
|
|
352 |
var tfoot = tableNode.children('tfoot');
|
|
|
353 |
var tbody = dom.tbody;
|
|
|
354 |
|
|
|
355 |
position.visible = tableNode.is(':visible');
|
|
|
356 |
position.width = tableNode.outerWidth();
|
|
|
357 |
position.left = tableNode.offset().left;
|
|
|
358 |
position.theadTop = thead.offset().top;
|
|
|
359 |
position.tbodyTop = tbody.offset().top;
|
|
|
360 |
position.theadHeight = position.tbodyTop - position.theadTop;
|
|
|
361 |
|
|
|
362 |
if ( tfoot.length ) {
|
|
|
363 |
position.tfootTop = tfoot.offset().top;
|
|
|
364 |
position.tfootBottom = position.tfootTop + tfoot.outerHeight();
|
|
|
365 |
position.tfootHeight = position.tfootBottom - position.tfootTop;
|
|
|
366 |
}
|
|
|
367 |
else {
|
|
|
368 |
position.tfootTop = position.tbodyTop + tbody.outerHeight();
|
|
|
369 |
position.tfootBottom = position.tfootTop;
|
|
|
370 |
position.tfootHeight = position.tfootTop;
|
|
|
371 |
}
|
|
|
372 |
},
|
|
|
373 |
|
|
|
374 |
|
|
|
375 |
/**
|
|
|
376 |
* Mode calculation - determine what mode the fixed items should be placed
|
|
|
377 |
* into.
|
|
|
378 |
*
|
|
|
379 |
* @param {boolean} forceChange Force a redraw of the mode, even if already
|
|
|
380 |
* in that mode.
|
|
|
381 |
* @private
|
|
|
382 |
*/
|
|
|
383 |
_scroll: function ( forceChange )
|
|
|
384 |
{
|
|
|
385 |
var windowTop = $(document).scrollTop();
|
|
|
386 |
var position = this.s.position;
|
|
|
387 |
var headerMode, footerMode;
|
|
|
388 |
|
|
|
389 |
if ( this.c.header ) {
|
|
|
390 |
if ( ! position.visible || windowTop <= position.theadTop - this.c.headerOffset ) {
|
|
|
391 |
headerMode = 'in-place';
|
|
|
392 |
}
|
|
|
393 |
else if ( windowTop <= position.tfootTop - position.theadHeight - this.c.headerOffset ) {
|
|
|
394 |
headerMode = 'in';
|
|
|
395 |
}
|
|
|
396 |
else {
|
|
|
397 |
headerMode = 'below';
|
|
|
398 |
}
|
|
|
399 |
|
|
|
400 |
if ( forceChange || headerMode !== this.s.headerMode ) {
|
|
|
401 |
this._modeChange( headerMode, 'header', forceChange );
|
|
|
402 |
}
|
|
|
403 |
}
|
|
|
404 |
|
|
|
405 |
if ( this.c.footer && this.dom.tfoot.length ) {
|
|
|
406 |
if ( ! position.visible || windowTop + position.windowHeight >= position.tfootBottom + this.c.footerOffset ) {
|
|
|
407 |
footerMode = 'in-place';
|
|
|
408 |
}
|
|
|
409 |
else if ( position.windowHeight + windowTop > position.tbodyTop + position.tfootHeight + this.c.footerOffset ) {
|
|
|
410 |
footerMode = 'in';
|
|
|
411 |
}
|
|
|
412 |
else {
|
|
|
413 |
footerMode = 'above';
|
|
|
414 |
}
|
|
|
415 |
|
|
|
416 |
if ( forceChange || footerMode !== this.s.footerMode ) {
|
|
|
417 |
this._modeChange( footerMode, 'footer', forceChange );
|
|
|
418 |
}
|
|
|
419 |
}
|
|
|
420 |
}
|
|
|
421 |
};
|
|
|
422 |
|
|
|
423 |
|
|
|
424 |
/**
|
|
|
425 |
* Version
|
|
|
426 |
* @type {String}
|
|
|
427 |
* @static
|
|
|
428 |
*/
|
|
|
429 |
FixedHeader.version = "3.0.0";
|
|
|
430 |
|
|
|
431 |
/**
|
|
|
432 |
* Defaults
|
|
|
433 |
* @type {Object}
|
|
|
434 |
* @static
|
|
|
435 |
*/
|
|
|
436 |
FixedHeader.defaults = {
|
|
|
437 |
header: true,
|
|
|
438 |
footer: false,
|
|
|
439 |
headerOffset: 0,
|
|
|
440 |
footerOffset: 0
|
|
|
441 |
};
|
|
|
442 |
|
|
|
443 |
|
|
|
444 |
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
|
|
445 |
* DataTables interfaces
|
|
|
446 |
*/
|
|
|
447 |
|
|
|
448 |
// Attach for constructor access
|
|
|
449 |
$.fn.dataTable.FixedHeader = FixedHeader;
|
|
|
450 |
$.fn.DataTable.FixedHeader = FixedHeader;
|
|
|
451 |
|
|
|
452 |
|
|
|
453 |
// DataTables creation - check if the FixedHeader option has been defined on the
|
|
|
454 |
// table and if so, initialise
|
|
|
455 |
$(document).on( 'init.dt.dtb', function (e, settings, json) {
|
|
|
456 |
if ( e.namespace !== 'dt' ) {
|
|
|
457 |
return;
|
|
|
458 |
}
|
|
|
459 |
|
|
|
460 |
var opts = settings.oInit.fixedHeader || DataTable.defaults.fixedHeader;
|
|
|
461 |
|
|
|
462 |
if ( opts && ! settings._buttons ) {
|
|
|
463 |
new FixedHeader( settings, opts );
|
|
|
464 |
}
|
|
|
465 |
} );
|
|
|
466 |
|
|
|
467 |
// DataTables API methods
|
|
|
468 |
DataTable.Api.register( 'fixedHeader()', function () {} );
|
|
|
469 |
|
|
|
470 |
DataTable.Api.register( 'fixedHeader.adjust()', function () {
|
|
|
471 |
return this.iterator( 'table', function ( ctx ) {
|
|
|
472 |
var fh = ctx._fixedHeader;
|
|
|
473 |
|
|
|
474 |
if ( fh ) {
|
|
|
475 |
fh.update();
|
|
|
476 |
}
|
|
|
477 |
} );
|
|
|
478 |
} );
|
|
|
479 |
|
|
|
480 |
|
|
|
481 |
return FixedHeader;
|
|
|
482 |
}; // /factory
|
|
|
483 |
|
|
|
484 |
|
|
|
485 |
// Define as an AMD module if possible
|
|
|
486 |
if ( typeof define === 'function' && define.amd ) {
|
|
|
487 |
define( ['jquery', 'datatables'], factory );
|
|
|
488 |
}
|
|
|
489 |
else if ( typeof exports === 'object' ) {
|
|
|
490 |
// Node/CommonJS
|
|
|
491 |
factory( require('jquery'), require('datatables') );
|
|
|
492 |
}
|
|
|
493 |
else if ( jQuery && !jQuery.fn.dataTable.FixedHeader ) {
|
|
|
494 |
// Otherwise simply initialise as normal, stopping multiple evaluation
|
|
|
495 |
factory( jQuery, jQuery.fn.dataTable );
|
|
|
496 |
}
|
|
|
497 |
|
|
|
498 |
|
|
|
499 |
})(window, document);
|