3 * Sticky table headers.
6 (function($, Drupal, displace) {
8 * Constructor for the tableHeader object. Provides sticky table headers.
10 * TableHeader will make the current table header stick to the top of the page
11 * if the table is very long.
13 * @constructor Drupal.TableHeader
15 * @param {HTMLElement} table
16 * DOM object for the table to add a sticky header to.
18 * @listens event:columnschange
20 function TableHeader(table) {
21 const $table = $(table);
24 * @name Drupal.TableHeader#$originalTable
28 this.$originalTable = $table;
33 this.$originalHeader = $table.children('thead');
38 this.$originalHeaderCells = this.$originalHeader.find('> tr > th');
43 this.displayWeight = null;
44 this.$originalTable.addClass('sticky-table');
45 this.tableHeight = $table[0].clientHeight;
46 this.tableOffset = this.$originalTable.offset();
48 // React to columns change to avoid making checks in the scroll callback.
49 this.$originalTable.on(
51 { tableHeader: this },
53 const tableHeader = e.data.tableHeader;
55 tableHeader.displayWeight === null ||
56 tableHeader.displayWeight !== display
58 tableHeader.recalculateSticky();
60 tableHeader.displayWeight = display;
64 // Create and display sticky header.
68 // Helper method to loop through tables and execute a method.
69 function forTables(method, arg) {
70 const tables = TableHeader.tables;
71 const il = tables.length;
72 for (let i = 0; i < il; i++) {
73 tables[i][method](arg);
77 // Select and initialize sticky table headers.
78 function tableHeaderInitHandler(e) {
79 const $tables = $(e.data.context)
80 .find('table.sticky-enabled')
82 const il = $tables.length;
83 for (let i = 0; i < il; i++) {
84 TableHeader.tables.push(new TableHeader($tables[i]));
86 forTables('onScroll');
90 * Attaches sticky table headers.
92 * @type {Drupal~behavior}
94 * @prop {Drupal~behaviorAttach} attach
95 * Attaches the sticky table header behavior.
97 Drupal.behaviors.tableHeader = {
100 'scroll.TableHeaderInit',
102 tableHeaderInitHandler,
107 function scrollValue(position) {
108 return document.documentElement[position] || document.body[position];
111 function tableHeaderResizeHandler(e) {
112 forTables('recalculateSticky');
115 function tableHeaderOnScrollHandler(e) {
116 forTables('onScroll');
119 function tableHeaderOffsetChangeHandler(e, offsets) {
120 forTables('stickyPosition', offsets.top);
123 // Bind event that need to change all tables.
126 * When resizing table width can change, recalculate everything.
130 'resize.TableHeader': tableHeaderResizeHandler,
133 * Bind only one event to take care of calling all scroll callbacks.
137 'scroll.TableHeader': tableHeaderOnScrollHandler,
139 // Bind to custom Drupal events.
142 * Recalculate columns width when window is resized and when show/hide
143 * weight is triggered.
147 'columnschange.TableHeader': tableHeaderResizeHandler,
150 * Recalculate TableHeader.topOffset when viewport is resized.
154 'drupalViewportOffsetChange.TableHeader': tableHeaderOffsetChangeHandler,
158 * Store the state of TableHeader.
162 /** @lends Drupal.TableHeader */ {
164 * This will store the state of all processed tables.
166 * @type {Array.<Drupal.TableHeader>}
173 * Extend TableHeader prototype.
176 TableHeader.prototype,
177 /** @lends Drupal.TableHeader# */ {
179 * Minimum height in pixels for the table to have a sticky header.
186 * Absolute position of the table on the page.
188 * @type {?Drupal~displaceOffset}
193 * Absolute position of the table on the page.
200 * Boolean storing the sticky header visibility state.
204 stickyVisible: false,
207 * Create the duplicate header.
210 // Clone the table header so it inherits original jQuery properties.
211 const $stickyHeader = this.$originalHeader.clone(true);
212 // Hide the table to avoid a flash of the header clone upon page load.
213 this.$stickyTable = $('<table class="sticky-header"/>')
215 visibility: 'hidden',
219 .append($stickyHeader)
220 .insertBefore(this.$originalTable);
222 this.$stickyHeaderCells = $stickyHeader.find('> tr > th');
224 // Initialize all computations.
225 this.recalculateSticky();
229 * Set absolute position of sticky.
231 * @param {number} offsetTop
232 * The top offset for the sticky header.
233 * @param {number} offsetLeft
234 * The left offset for the sticky header.
237 * The sticky table as a jQuery collection.
239 stickyPosition(offsetTop, offsetLeft) {
241 if (typeof offsetTop === 'number') {
242 css.top = `${offsetTop}px`;
244 if (typeof offsetLeft === 'number') {
245 css.left = `${this.tableOffset.left - offsetLeft}px`;
247 return this.$stickyTable.css(css);
251 * Returns true if sticky is currently visible.
254 * The visibility status.
256 checkStickyVisible() {
257 const scrollTop = scrollValue('scrollTop');
258 const tableTop = this.tableOffset.top - displace.offsets.top;
259 const tableBottom = tableTop + this.tableHeight;
262 if (tableTop < scrollTop && scrollTop < tableBottom - this.minHeight) {
266 this.stickyVisible = visible;
271 * Check if sticky header should be displayed.
273 * This function is throttled to once every 250ms to avoid unnecessary
276 * @param {jQuery.Event} e
280 this.checkStickyVisible();
281 // Track horizontal positioning relative to the viewport.
282 this.stickyPosition(null, scrollValue('scrollLeft'));
283 this.$stickyTable.css(
285 this.stickyVisible ? 'visible' : 'hidden',
290 * Event handler: recalculates position of the sticky table header.
292 * @param {jQuery.Event} event
293 * Event being triggered.
295 recalculateSticky(event) {
296 // Update table size.
297 this.tableHeight = this.$originalTable[0].clientHeight;
299 // Update offset top.
300 displace.offsets.top = displace.calculateOffset('top');
301 this.tableOffset = this.$originalTable.offset();
302 this.stickyPosition(displace.offsets.top, scrollValue('scrollLeft'));
304 // Update columns width.
306 let $stickyCell = null;
308 // Resize header and its cell widths.
309 // Only apply width to visible table cells. This prevents the header from
310 // displaying incorrectly when the sticky header is no longer visible.
311 const il = this.$originalHeaderCells.length;
312 for (let i = 0; i < il; i++) {
313 $that = $(this.$originalHeaderCells[i]);
314 $stickyCell = this.$stickyHeaderCells.eq($that.index());
315 display = $that.css('display');
316 if (display !== 'none') {
317 $stickyCell.css({ width: $that.css('width'), display });
319 $stickyCell.css('display', 'none');
322 this.$stickyTable.css('width', this.$originalTable.outerWidth());
327 // Expose constructor in the public space.
328 Drupal.TableHeader = TableHeader;
329 })(jQuery, Drupal, window.parent.Drupal.displace);