').addClass(this.classes.bottomBlocker),
121 | topBlocker: $('
').addClass(this.classes.topBlocker),
122 | scroller: $('div.dt-scroll-body', this.s.dt.table().container()),
123 | };
124 |
125 | if (this.s.dt.settings()[0]._bInitComplete) {
126 | // Fixed Columns Initialisation
127 | this._addStyles();
128 | this._setKeyTableListener();
129 | }
130 | else {
131 | table.one('init.dt.dtfc', () => {
132 | // Fixed Columns Initialisation
133 | this._addStyles();
134 | this._setKeyTableListener();
135 | });
136 | }
137 |
138 | // Lots or reasons to redraw the column styles
139 | table.on(
140 | 'column-sizing.dt.dtfc column-reorder.dt.dtfc draw.dt.dtfc',
141 | () => this._addStyles()
142 | );
143 |
144 | // Column visibility can trigger a number of times quickly, so we debounce it
145 | let debounced = DataTable.util.debounce(() => {
146 | this._addStyles()
147 | }, 50);
148 |
149 | table.on('column-visibility.dt.dtfc', () => {
150 | debounced();
151 | });
152 |
153 | // Add classes to indicate scrolling state for styling
154 | this.dom.scroller.on('scroll.dtfc', () => this._scroll());
155 | this._scroll();
156 |
157 | // Make class available through dt object
158 | table.settings()[0]._fixedColumns = this;
159 |
160 | table.on('destroy', () => this._destroy());
161 |
162 | return this;
163 | }
164 |
165 | /**
166 | * Getter for the `fixedColumns.end` property
167 | *
168 | * @param newVal Optional. If present this will be the new value for the number of end fixed columns
169 | * @returns The number of end fixed columns
170 | */
171 | public end(): number;
172 | /**
173 | * Setter for the `fixedColumns.right` property
174 | *
175 | * @param newVal The new value for the number of right fixed columns
176 | * @returns DataTables API for chaining
177 | */
178 | public end(newVal: number): any;
179 | public end(newVal?: number): any {
180 | // If the value is to change
181 | if (newVal !== undefined) {
182 | if (newVal >= 0 && newVal <= this.s.dt.columns().count()) {
183 | // Set the new values and redraw the columns
184 | this.c.end = newVal;
185 | this._addStyles();
186 | }
187 |
188 | return this;
189 | }
190 |
191 | return this.c.end;
192 | }
193 |
194 | /**
195 | * Left fix - accounting for RTL
196 | *
197 | * @param count Columns to fix, or undefined for getter
198 | */
199 | public left(count?) {
200 | return this.s.rtl
201 | ? this.end(count)
202 | : this.start(count);
203 | }
204 |
205 | /**
206 | * Right fix - accounting for RTL
207 | *
208 | * @param count Columns to fix, or undefined for getter
209 | */
210 | public right(count?) {
211 | return this.s.rtl
212 | ? this.start(count)
213 | : this.end(count);
214 | }
215 |
216 | /**
217 | * Getter for the `fixedColumns.start` property
218 | *
219 | * @param newVal Optional. If present this will be the new value for the number of start fixed columns
220 | * @returns The number of start fixed columns
221 | */
222 | public start(): number;
223 | /**
224 | * Setter for the `fixedColumns.start` property
225 | *
226 | * @param newVal The new value for the number of left fixed columns
227 | * @returns DataTables API for chaining
228 | */
229 | public start(newVal: number): any;
230 | public start(newVal?: number): any {
231 | // If the value is to change
232 | if (newVal !== undefined) {
233 | if (newVal >= 0 && newVal <= this.s.dt.columns().count()) {
234 | // Set the new values and redraw the columns
235 | this.c.start = newVal;
236 | this._addStyles();
237 | }
238 |
239 | return this;
240 | }
241 |
242 | return this.c.start;
243 | }
244 |
245 | /**
246 | * Iterates over the columns, fixing the appropriate ones to the left and right
247 | */
248 | private _addStyles() {
249 | let dt = this.s.dt;
250 | let that = this;
251 | let colCount = this.s.dt.columns(':visible').count();
252 | let headerStruct = dt.table().header.structure(':visible');
253 | let footerStruct = dt.table().footer.structure(':visible');
254 | let widths = dt.columns(':visible').widths().toArray();
255 | let wrapper = $(dt.table().node()).closest('div.dt-scroll');
256 | let scroller = $(dt.table().node()).closest('div.dt-scroll-body')[0];
257 | let rtl = this.s.rtl;
258 | let start = this.c.start;
259 | let end = this.c.end;
260 | let left = rtl ? end : start;
261 | let right = rtl ? start : end;
262 | let barWidth = dt.settings()[0].oBrowser.barWidth; // dt internal
263 |
264 | // Do nothing if no scrolling in the DataTable
265 | if (wrapper.length === 0) {
266 | return this;
267 | }
268 |
269 | // Bar not needed - no vertical scrolling
270 | if (scroller.offsetWidth === scroller.clientWidth) {
271 | barWidth = 0;
272 | }
273 |
274 | // Loop over the visible columns, setting their state
275 | dt.columns().every(function (colIdx) {
276 | let visIdx = dt.column.index('toVisible', colIdx);
277 | let offset;
278 |
279 | // Skip the hidden columns
280 | if (visIdx === null) {
281 | return;
282 | }
283 |
284 | if (visIdx < start) {
285 | // Fix to the start
286 | offset = that._sum(widths, visIdx);
287 |
288 | that._fixColumn(
289 | visIdx,
290 | offset,
291 | 'start',
292 | headerStruct,
293 | footerStruct,
294 | barWidth
295 | );
296 | }
297 | else if (visIdx >= colCount - end) {
298 | // Fix to the end
299 | offset = that._sum(
300 | widths,
301 | colCount - visIdx - 1,
302 | true
303 | );
304 |
305 | that._fixColumn(
306 | visIdx,
307 | offset,
308 | 'end',
309 | headerStruct,
310 | footerStruct,
311 | barWidth
312 | );
313 | }
314 | else {
315 | // Release
316 | that._fixColumn(visIdx, 0, 'none', headerStruct, footerStruct, barWidth);
317 | }
318 | });
319 |
320 | // Apply classes to table to indicate what state we are in
321 | $(dt.table().node())
322 | .toggleClass(that.classes.tableFixedStart, start > 0)
323 | .toggleClass(that.classes.tableFixedEnd, end > 0)
324 | .toggleClass(that.classes.tableFixedLeft, left > 0)
325 | .toggleClass(that.classes.tableFixedRight, right > 0);
326 |
327 | // Blocker elements for when scroll bars are always visible
328 | let headerEl = dt.table().header();
329 | let footerEl = dt.table().footer();
330 | let headerHeight = $(headerEl).outerHeight();
331 | let footerHeight = $(footerEl).outerHeight();
332 |
333 | this.dom.topBlocker
334 | .appendTo(wrapper)
335 | .css('top', 0)
336 | .css(this.s.rtl ? 'left' : 'right', 0)
337 | .css('height', headerHeight)
338 | .css('width', barWidth + 1)
339 | .css('display', barWidth ? 'block' : 'none');
340 |
341 | if (footerEl) {
342 | this.dom.bottomBlocker
343 | .appendTo(wrapper)
344 | .css('bottom', 0)
345 | .css(this.s.rtl ? 'left' : 'right', 0)
346 | .css('height', footerHeight)
347 | .css('width', barWidth + 1)
348 | .css('display', barWidth ? 'block' : 'none');
349 | }
350 | }
351 |
352 | /**
353 | * Clean up
354 | */
355 | private _destroy() {
356 | this.s.dt.off('.dtfc');
357 | this.dom.scroller.off('.dtfc');
358 |
359 | $(this.s.dt.table().node())
360 | .removeClass(
361 | this.classes.tableScrollingEnd + ' ' +
362 | this.classes.tableScrollingLeft + ' ' +
363 | this.classes.tableScrollingStart + ' ' +
364 | this.classes.tableScrollingRight
365 | );
366 |
367 | this.dom.bottomBlocker.remove();
368 | this.dom.topBlocker.remove();
369 | }
370 |
371 | /**
372 | * Fix or unfix a column
373 | *
374 | * @param idx Column visible index to operate on
375 | * @param offset Offset from the start (pixels)
376 | * @param side start, end or none to unfix a column
377 | * @param header DT header structure object
378 | * @param footer DT footer structure object
379 | */
380 | private _fixColumn(
381 | idx: number,
382 | offset: number,
383 | side: 'start' | 'end' | 'none',
384 | header,
385 | footer,
386 | barWidth
387 | ) {
388 | let dt = this.s.dt;
389 | let applyStyles = (jq, part) => {
390 | if (side === 'none') {
391 | jq.css('position', '')
392 | .css('left', '')
393 | .css('right', '')
394 | .removeClass(
395 | this.classes.fixedEnd + ' ' +
396 | this.classes.fixedLeft + ' ' +
397 | this.classes.fixedRight + ' ' +
398 | this.classes.fixedStart
399 | );
400 | }
401 | else {
402 | let positionSide = side === 'start' ? 'left' : 'right';
403 |
404 | if (this.s.rtl) {
405 | positionSide = side === 'start' ? 'right' : 'left';
406 | }
407 |
408 | var off = offset;
409 |
410 | if (side === 'end' && (part === 'header' || part === 'footer')) {
411 | off += barWidth;
412 | }
413 |
414 | jq.css('position', 'sticky')
415 | .css(positionSide, off)
416 | .addClass(
417 | side === 'start'
418 | ? this.classes.fixedStart
419 | : this.classes.fixedEnd
420 | )
421 | .addClass(
422 | positionSide === 'left'
423 | ? this.classes.fixedLeft
424 | : this.classes.fixedRight
425 | );
426 | }
427 | };
428 |
429 | header.forEach((row) => {
430 | if (row[idx]) {
431 | applyStyles($(row[idx].cell), 'header');
432 | }
433 | });
434 |
435 | applyStyles(dt.column(idx + ':visible', { page: 'current' }).nodes().to$(), 'body');
436 |
437 | if (footer) {
438 | footer.forEach((row) => {
439 | if (row[idx]) {
440 | applyStyles($(row[idx].cell), 'footer');
441 | }
442 | });
443 | }
444 | }
445 |
446 | /**
447 | * Update classes on the table to indicate if the table is scrolling or not
448 | */
449 | private _scroll() {
450 | let scroller = this.dom.scroller[0];
451 |
452 | // Not a scrolling table
453 | if (! scroller) {
454 | return;
455 | }
456 |
457 | // Need to update the classes on potentially multiple table tags. There is the
458 | // main one, the scrolling ones and if FixedHeader is active, the holding
459 | // position ones! jQuery will deduplicate for us.
460 | let table = $(this.s.dt.table().node())
461 | .add(this.s.dt.table().header().parentNode)
462 | .add(this.s.dt.table().footer().parentNode)
463 | .add('div.dt-scroll-headInner table', this.s.dt.table().container())
464 | .add('div.dt-scroll-footInner table', this.s.dt.table().container());
465 |
466 | let scrollLeft = scroller.scrollLeft; // 0 when fully scrolled left
467 | let ltr = ! this.s.rtl;
468 | let scrollStart = scrollLeft !== 0;
469 | let scrollEnd = scroller.scrollWidth > (scroller.clientWidth + Math.abs(scrollLeft) + 1); // extra 1 for Chrome
470 |
471 | table.toggleClass(this.classes.tableScrollingStart, scrollStart);
472 | table.toggleClass(this.classes.tableScrollingEnd, scrollEnd);
473 | table.toggleClass(this.classes.tableScrollingLeft, (scrollStart && ltr) || (scrollEnd && ! ltr));
474 | table.toggleClass(this.classes.tableScrollingRight, (scrollEnd && ltr) || (scrollStart && ! ltr));
475 | }
476 |
477 | private _setKeyTableListener() {
478 | this.s.dt.on('key-focus.dt.dtfc', (e, dt, cell) => {
479 | let currScroll;
480 | let cellPos = $(cell.node()).offset();
481 | let scroller = this.dom.scroller[0];
482 | let scroll = $(
483 | $(this.s.dt.table().node()).closest('div.dt-scroll-body')
484 | );
485 |
486 | // If there are fixed columns to the left
487 | if (this.c.start > 0) {
488 | // Get the rightmost left fixed column header, it's position and it's width
489 | let rightMost = $(this.s.dt.column(this.c.start - 1).header());
490 | let rightMostPos = rightMost.offset();
491 | let rightMostWidth = rightMost.outerWidth();
492 |
493 | // If the current highlighted cell is left of the rightmost cell on the screen
494 | if ($(cell.node()).hasClass(this.classes.fixedLeft)) {
495 | // Fixed columns have the scrollbar at the start, always
496 | scroll.scrollLeft(0);
497 | }
498 | else if (cellPos.left < rightMostPos.left + rightMostWidth) {
499 | // Scroll it into view
500 | currScroll = scroll.scrollLeft();
501 | scroll.scrollLeft(
502 | currScroll -
503 | (rightMostPos.left + rightMostWidth - cellPos.left)
504 | );
505 | }
506 | }
507 |
508 | // If there are fixed columns to the right
509 | if (this.c.end > 0) {
510 | // Get the number of columns and the width of the cell as doing right side calc
511 | let numCols = this.s.dt.columns().data().toArray().length;
512 | let cellWidth = $(cell.node()).outerWidth();
513 |
514 | // Get the leftmost right fixed column header and it's position
515 | let leftMost = $(
516 | this.s.dt.column(numCols - this.c.end).header()
517 | );
518 | let leftMostPos = leftMost.offset();
519 |
520 | // If the current highlighted cell is right of the leftmost cell on the screen
521 | if ($(cell.node()).hasClass(this.classes.fixedRight)) {
522 | scroll.scrollLeft(scroller.scrollWidth - scroller.clientWidth);
523 | }
524 | else if (cellPos.left + cellWidth > leftMostPos.left) {
525 | // Scroll it into view
526 | currScroll = scroll.scrollLeft();
527 | scroll.scrollLeft(
528 | currScroll -
529 | (leftMostPos.left - (cellPos.left + cellWidth))
530 | );
531 | }
532 | }
533 | });
534 | }
535 |
536 | /**
537 | * Sum a range of values from an array
538 | *
539 | * @param widths
540 | * @param index
541 | * @returns
542 | */
543 | private _sum(widths: number[], index: number, reverse: boolean = false) {
544 | if (reverse) {
545 | widths = widths.slice().reverse();
546 | }
547 |
548 | return widths.slice(0, index).reduce((accum, val) => accum + val, 0);
549 | }
550 | }
551 |
--------------------------------------------------------------------------------
/src/fixedColumns.bootstrap.ts:
--------------------------------------------------------------------------------
1 | /*! Bootstrap integration for DataTables' FixedColumns
2 | * © SpryMedia Ltd - datatables.net/license
3 | */
4 |
--------------------------------------------------------------------------------
/src/fixedColumns.bootstrap4.ts:
--------------------------------------------------------------------------------
1 | /*! Bootstrap 4 integration for DataTables' FixedColumns
2 | * © SpryMedia Ltd - datatables.net/license
3 | */
4 |
--------------------------------------------------------------------------------
/src/fixedColumns.bootstrap5.ts:
--------------------------------------------------------------------------------
1 | /*! Bootstrap 5 integration for DataTables' FixedColumns
2 | * © SpryMedia Ltd - datatables.net/license
3 | */
4 |
--------------------------------------------------------------------------------
/src/fixedColumns.bulma.ts:
--------------------------------------------------------------------------------
1 | /*! Bulma integration for DataTables' FixedColumns
2 | * © SpryMedia Ltd - datatables.net/license
3 | */
4 |
--------------------------------------------------------------------------------
/src/fixedColumns.dataTables.ts:
--------------------------------------------------------------------------------
1 |
2 | /*! DataTables integration for DataTables' FixedColumns
3 | * © SpryMedia Ltd - datatables.net/license
4 | */
5 |
--------------------------------------------------------------------------------
/src/fixedColumns.foundation.ts:
--------------------------------------------------------------------------------
1 | /*! Foundation integration for DataTables' FixedColumns
2 | * © SpryMedia Ltd - datatables.net/license
3 | */
4 |
--------------------------------------------------------------------------------
/src/fixedColumns.jqueryui.ts:
--------------------------------------------------------------------------------
1 | /*! jquery ui integration for DataTables' FixedColumns
2 | * © SpryMedia Ltd - datatables.net/license
3 | */
4 |
--------------------------------------------------------------------------------
/src/fixedColumns.semanticui.ts:
--------------------------------------------------------------------------------
1 | /*! Semantic ui integration for DataTables' FixedColumns
2 | * © SpryMedia Ltd - datatables.net/license
3 | */
4 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | /*! FixedColumns 5.0.4
2 | * © SpryMedia Ltd - datatables.net/license
3 | */
4 |
5 | /**
6 | * @summary FixedColumns
7 | * @description FixedColumns extension for DataTables
8 | * @version 5.0.4
9 | * @author SpryMedia Ltd (www.sprymedia.co.uk)
10 | * @copyright SpryMedia Ltd.
11 | *
12 | * This source file is free software, available under the following license:
13 | * MIT license - http://datatables.net/license/mit
14 | *
15 | * This source file is distributed in the hope that it will be useful, but
16 | * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
17 | * or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details.
18 | *
19 | * For details please refer to: http://www.datatables.net
20 | */
21 |
22 | ///
7 |
8 | import DataTables, {Api} from 'datatables.net';
9 |
10 | export default DataTables;
11 |
12 | declare module 'datatables.net' {
13 | interface Config {
14 | /*
15 | * FixedColumns extension options
16 | */
17 | fixedColumns?: boolean | FixedColumnsConfig;
18 | }
19 |
20 | interface FixedColumnsConfig {
21 | /**
22 | * The number of columns to fix at the end of the table (ltr and rtl aware)
23 | */
24 | end?: number;
25 |
26 | i18n?: {
27 | /** Text for `fixedColumns` button */
28 | button?: string;
29 | };
30 |
31 | /**
32 | * The number of columns on the left hand side of the table to fix in place.
33 | */
34 | left?: number;
35 |
36 | /**
37 | * The number of columns on the left hand side of the table to fix in place.
38 | * @deprecated Use `start`
39 | */
40 | leftColumns?: number;
41 |
42 | /**
43 | * The number of columns on the right hand side of the table to fix in place.
44 | */
45 | right?: number;
46 |
47 | /**
48 | * The number of columns on the right hand side of the table to fix in place.
49 | * @deprecated Use `end`
50 | */
51 | rightColumns?: number;
52 |
53 | /**
54 | * The number of columns to fix at the start of the table (ltr and rtl aware)
55 | */
56 | start?: number;
57 | }
58 |
59 | interface Api {
60 | /**
61 | * Namespacing for FixedColumns methods - FixedColumns' methods are available on the returned API instance.
62 | *
63 | * @returns DataTables API instance with the FixedColumns methods available.
64 | */
65 | fixedColumns(): FixedColumnsMethods;
66 | }
67 |
68 | interface FixedColumnsMethods extends Api {
69 | /**
70 | * Get the number of columns fixed at the end of the table
71 | *
72 | * @returns Count
73 | */
74 | end(): number;
75 |
76 | /**
77 | * Set the number of columns fixed at the end of the table
78 | *
79 | * @returns DataTables API instance
80 | */
81 | end(count: number): Api;
82 |
83 | /**
84 | * Get the number of columns fixed at the left of the table
85 | *
86 | * @returns Count
87 | */
88 | left(): number;
89 |
90 | /**
91 | * Set the number of columns fixed at the left of the table
92 | *
93 | * @returns DataTables API instance
94 | */
95 | left(count: number): Api;
96 |
97 | /**
98 | * Get the number of columns fixed at the right of the table
99 | *
100 | * @returns Count
101 | */
102 | right(): number;
103 |
104 | /**
105 | * Set the number of columns fixed at the right of the table
106 | *
107 | * @returns DataTables API instance
108 | */
109 | right(count: number): Api;
110 |
111 | /**
112 | * Get the number of columns fixed at the start of the table
113 | *
114 | * @returns Count
115 | */
116 | start(): number;
117 |
118 | /**
119 | * Set the number of columns fixed at the start of the table
120 | *
121 | * @returns DataTables API instance
122 | */
123 | start(count: number): Api;
124 | }
125 | }
126 |
--------------------------------------------------------------------------------