3 * Sticky table headers.
6 (function ($, Drupal, displace) {
8 * Attaches sticky table headers.
10 * @type {Drupal~behavior}
12 * @prop {Drupal~behaviorAttach} attach
13 * Attaches the sticky table header behavior.
15 Drupal.behaviors.tableHeader = {
17 $(window).one('scroll.TableHeaderInit', { context }, tableHeaderInitHandler);
21 function scrollValue(position) {
22 return document.documentElement[position] || document.body[position];
25 // Select and initialize sticky table headers.
26 function tableHeaderInitHandler(e) {
27 const $tables = $(e.data.context).find('table.sticky-enabled').once('tableheader');
28 const il = $tables.length;
29 for (let i = 0; i < il; i++) {
30 TableHeader.tables.push(new TableHeader($tables[i]));
32 forTables('onScroll');
35 // Helper method to loop through tables and execute a method.
36 function forTables(method, arg) {
37 const tables = TableHeader.tables;
38 const il = tables.length;
39 for (let i = 0; i < il; i++) {
40 tables[i][method](arg);
44 function tableHeaderResizeHandler(e) {
45 forTables('recalculateSticky');
48 function tableHeaderOnScrollHandler(e) {
49 forTables('onScroll');
52 function tableHeaderOffsetChangeHandler(e, offsets) {
53 forTables('stickyPosition', offsets.top);
56 // Bind event that need to change all tables.
60 * When resizing table width can change, recalculate everything.
64 'resize.TableHeader': tableHeaderResizeHandler,
67 * Bind only one event to take care of calling all scroll callbacks.
71 'scroll.TableHeader': tableHeaderOnScrollHandler,
73 // Bind to custom Drupal events.
77 * Recalculate columns width when window is resized and when show/hide
78 * weight is triggered.
82 'columnschange.TableHeader': tableHeaderResizeHandler,
85 * Recalculate TableHeader.topOffset when viewport is resized.
89 'drupalViewportOffsetChange.TableHeader': tableHeaderOffsetChangeHandler,
93 * Constructor for the tableHeader object. Provides sticky table headers.
95 * TableHeader will make the current table header stick to the top of the page
96 * if the table is very long.
98 * @constructor Drupal.TableHeader
100 * @param {HTMLElement} table
101 * DOM object for the table to add a sticky header to.
103 * @listens event:columnschange
105 function TableHeader(table) {
106 const $table = $(table);
109 * @name Drupal.TableHeader#$originalTable
111 * @type {HTMLElement}
113 this.$originalTable = $table;
118 this.$originalHeader = $table.children('thead');
123 this.$originalHeaderCells = this.$originalHeader.find('> tr > th');
128 this.displayWeight = null;
129 this.$originalTable.addClass('sticky-table');
130 this.tableHeight = $table[0].clientHeight;
131 this.tableOffset = this.$originalTable.offset();
133 // React to columns change to avoid making checks in the scroll callback.
134 this.$originalTable.on('columnschange', { tableHeader: this }, (e, display) => {
135 const tableHeader = e.data.tableHeader;
136 if (tableHeader.displayWeight === null || tableHeader.displayWeight !== display) {
137 tableHeader.recalculateSticky();
139 tableHeader.displayWeight = display;
142 // Create and display sticky header.
147 * Store the state of TableHeader.
149 $.extend(TableHeader, /** @lends Drupal.TableHeader */{
152 * This will store the state of all processed tables.
154 * @type {Array.<Drupal.TableHeader>}
160 * Extend TableHeader prototype.
162 $.extend(TableHeader.prototype, /** @lends Drupal.TableHeader# */{
165 * Minimum height in pixels for the table to have a sticky header.
172 * Absolute position of the table on the page.
174 * @type {?Drupal~displaceOffset}
179 * Absolute position of the table on the page.
186 * Boolean storing the sticky header visibility state.
190 stickyVisible: false,
193 * Create the duplicate header.
196 // Clone the table header so it inherits original jQuery properties.
197 const $stickyHeader = this.$originalHeader.clone(true);
198 // Hide the table to avoid a flash of the header clone upon page load.
199 this.$stickyTable = $('<table class="sticky-header"/>')
201 visibility: 'hidden',
205 .append($stickyHeader)
206 .insertBefore(this.$originalTable);
208 this.$stickyHeaderCells = $stickyHeader.find('> tr > th');
210 // Initialize all computations.
211 this.recalculateSticky();
215 * Set absolute position of sticky.
217 * @param {number} offsetTop
218 * The top offset for the sticky header.
219 * @param {number} offsetLeft
220 * The left offset for the sticky header.
223 * The sticky table as a jQuery collection.
225 stickyPosition(offsetTop, offsetLeft) {
227 if (typeof offsetTop === 'number') {
228 css.top = `${offsetTop}px`;
230 if (typeof offsetLeft === 'number') {
231 css.left = `${this.tableOffset.left - offsetLeft}px`;
233 return this.$stickyTable.css(css);
237 * Returns true if sticky is currently visible.
240 * The visibility status.
242 checkStickyVisible() {
243 const scrollTop = scrollValue('scrollTop');
244 const tableTop = this.tableOffset.top - displace.offsets.top;
245 const tableBottom = tableTop + this.tableHeight;
248 if (tableTop < scrollTop && scrollTop < (tableBottom - this.minHeight)) {
252 this.stickyVisible = visible;
257 * Check if sticky header should be displayed.
259 * This function is throttled to once every 250ms to avoid unnecessary
262 * @param {jQuery.Event} e
266 this.checkStickyVisible();
267 // Track horizontal positioning relative to the viewport.
268 this.stickyPosition(null, scrollValue('scrollLeft'));
269 this.$stickyTable.css('visibility', this.stickyVisible ? 'visible' : 'hidden');
273 * Event handler: recalculates position of the sticky table header.
275 * @param {jQuery.Event} event
276 * Event being triggered.
278 recalculateSticky(event) {
279 // Update table size.
280 this.tableHeight = this.$originalTable[0].clientHeight;
282 // Update offset top.
283 displace.offsets.top = displace.calculateOffset('top');
284 this.tableOffset = this.$originalTable.offset();
285 this.stickyPosition(displace.offsets.top, scrollValue('scrollLeft'));
287 // Update columns width.
289 let $stickyCell = null;
291 // Resize header and its cell widths.
292 // Only apply width to visible table cells. This prevents the header from
293 // displaying incorrectly when the sticky header is no longer visible.
294 const il = this.$originalHeaderCells.length;
295 for (let i = 0; i < il; i++) {
296 $that = $(this.$originalHeaderCells[i]);
297 $stickyCell = this.$stickyHeaderCells.eq($that.index());
298 display = $that.css('display');
299 if (display !== 'none') {
300 $stickyCell.css({ width: $that.css('width'), display });
303 $stickyCell.css('display', 'none');
306 this.$stickyTable.css('width', this.$originalTable.outerWidth());
310 // Expose constructor in the public space.
311 Drupal.TableHeader = TableHeader;
312 }(jQuery, Drupal, window.parent.Drupal.displace));