3 * Sticky table headers.
6 (function ($, Drupal, displace) {
11 * Attaches sticky table headers.
13 * @type {Drupal~behavior}
15 * @prop {Drupal~behaviorAttach} attach
16 * Attaches the sticky table header behavior.
18 Drupal.behaviors.tableHeader = {
19 attach: function (context) {
20 $(window).one('scroll.TableHeaderInit', {context: context}, tableHeaderInitHandler);
24 function scrollValue(position) {
25 return document.documentElement[position] || document.body[position];
28 // Select and initialize sticky table headers.
29 function tableHeaderInitHandler(e) {
30 var $tables = $(e.data.context).find('table.sticky-enabled').once('tableheader');
31 var il = $tables.length;
32 for (var i = 0; i < il; i++) {
33 TableHeader.tables.push(new TableHeader($tables[i]));
35 forTables('onScroll');
38 // Helper method to loop through tables and execute a method.
39 function forTables(method, arg) {
40 var tables = TableHeader.tables;
41 var il = tables.length;
42 for (var i = 0; i < il; i++) {
43 tables[i][method](arg);
47 function tableHeaderResizeHandler(e) {
48 forTables('recalculateSticky');
51 function tableHeaderOnScrollHandler(e) {
52 forTables('onScroll');
55 function tableHeaderOffsetChangeHandler(e, offsets) {
56 forTables('stickyPosition', offsets.top);
59 // Bind event that need to change all tables.
63 * When resizing table width can change, recalculate everything.
67 'resize.TableHeader': tableHeaderResizeHandler,
70 * Bind only one event to take care of calling all scroll callbacks.
74 'scroll.TableHeader': tableHeaderOnScrollHandler
76 // Bind to custom Drupal events.
80 * Recalculate columns width when window is resized and when show/hide
81 * weight is triggered.
85 'columnschange.TableHeader': tableHeaderResizeHandler,
88 * Recalculate TableHeader.topOffset when viewport is resized.
92 'drupalViewportOffsetChange.TableHeader': tableHeaderOffsetChangeHandler
96 * Constructor for the tableHeader object. Provides sticky table headers.
98 * TableHeader will make the current table header stick to the top of the page
99 * if the table is very long.
101 * @constructor Drupal.TableHeader
103 * @param {HTMLElement} table
104 * DOM object for the table to add a sticky header to.
106 * @listens event:columnschange
108 function TableHeader(table) {
109 var $table = $(table);
112 * @name Drupal.TableHeader#$originalTable
114 * @type {HTMLElement}
116 this.$originalTable = $table;
121 this.$originalHeader = $table.children('thead');
126 this.$originalHeaderCells = this.$originalHeader.find('> tr > th');
131 this.displayWeight = null;
132 this.$originalTable.addClass('sticky-table');
133 this.tableHeight = $table[0].clientHeight;
134 this.tableOffset = this.$originalTable.offset();
136 // React to columns change to avoid making checks in the scroll callback.
137 this.$originalTable.on('columnschange', {tableHeader: this}, function (e, display) {
138 var tableHeader = e.data.tableHeader;
139 if (tableHeader.displayWeight === null || tableHeader.displayWeight !== display) {
140 tableHeader.recalculateSticky();
142 tableHeader.displayWeight = display;
145 // Create and display sticky header.
150 * Store the state of TableHeader.
152 $.extend(TableHeader, /** @lends Drupal.TableHeader */{
155 * This will store the state of all processed tables.
157 * @type {Array.<Drupal.TableHeader>}
163 * Extend TableHeader prototype.
165 $.extend(TableHeader.prototype, /** @lends Drupal.TableHeader# */{
168 * Minimum height in pixels for the table to have a sticky header.
175 * Absolute position of the table on the page.
177 * @type {?Drupal~displaceOffset}
182 * Absolute position of the table on the page.
189 * Boolean storing the sticky header visibility state.
193 stickyVisible: false,
196 * Create the duplicate header.
198 createSticky: function () {
199 // Clone the table header so it inherits original jQuery properties.
200 var $stickyHeader = this.$originalHeader.clone(true);
201 // Hide the table to avoid a flash of the header clone upon page load.
202 this.$stickyTable = $('<table class="sticky-header"/>')
204 visibility: 'hidden',
208 .append($stickyHeader)
209 .insertBefore(this.$originalTable);
211 this.$stickyHeaderCells = $stickyHeader.find('> tr > th');
213 // Initialize all computations.
214 this.recalculateSticky();
218 * Set absolute position of sticky.
220 * @param {number} offsetTop
221 * The top offset for the sticky header.
222 * @param {number} offsetLeft
223 * The left offset for the sticky header.
226 * The sticky table as a jQuery collection.
228 stickyPosition: function (offsetTop, offsetLeft) {
230 if (typeof offsetTop === 'number') {
231 css.top = offsetTop + 'px';
233 if (typeof offsetLeft === 'number') {
234 css.left = (this.tableOffset.left - offsetLeft) + 'px';
236 return this.$stickyTable.css(css);
240 * Returns true if sticky is currently visible.
243 * The visibility status.
245 checkStickyVisible: function () {
246 var scrollTop = scrollValue('scrollTop');
247 var tableTop = this.tableOffset.top - displace.offsets.top;
248 var tableBottom = tableTop + this.tableHeight;
251 if (tableTop < scrollTop && scrollTop < (tableBottom - this.minHeight)) {
255 this.stickyVisible = visible;
260 * Check if sticky header should be displayed.
262 * This function is throttled to once every 250ms to avoid unnecessary
265 * @param {jQuery.Event} e
268 onScroll: function (e) {
269 this.checkStickyVisible();
270 // Track horizontal positioning relative to the viewport.
271 this.stickyPosition(null, scrollValue('scrollLeft'));
272 this.$stickyTable.css('visibility', this.stickyVisible ? 'visible' : 'hidden');
276 * Event handler: recalculates position of the sticky table header.
278 * @param {jQuery.Event} event
279 * Event being triggered.
281 recalculateSticky: function (event) {
282 // Update table size.
283 this.tableHeight = this.$originalTable[0].clientHeight;
285 // Update offset top.
286 displace.offsets.top = displace.calculateOffset('top');
287 this.tableOffset = this.$originalTable.offset();
288 this.stickyPosition(displace.offsets.top, scrollValue('scrollLeft'));
290 // Update columns width.
292 var $stickyCell = null;
294 // Resize header and its cell widths.
295 // Only apply width to visible table cells. This prevents the header from
296 // displaying incorrectly when the sticky header is no longer visible.
297 var il = this.$originalHeaderCells.length;
298 for (var i = 0; i < il; i++) {
299 $that = $(this.$originalHeaderCells[i]);
300 $stickyCell = this.$stickyHeaderCells.eq($that.index());
301 display = $that.css('display');
302 if (display !== 'none') {
303 $stickyCell.css({width: $that.css('width'), display: display});
306 $stickyCell.css('display', 'none');
309 this.$stickyTable.css('width', this.$originalTable.outerWidth());
313 // Expose constructor in the public space.
314 Drupal.TableHeader = TableHeader;
316 }(jQuery, Drupal, window.parent.Drupal.displace));