(function ($, Drupal) { "use strict"; /** * Attaches sticky table headers. */ Drupal.behaviors.tableHeader = { attach: function (context) { $(window).one('scroll.TableHeaderInit', {context: context}, tableHeaderInitHandler); } }; function scrollValue(position) { return document.documentElement[position] || document.body[position]; } // Select and initilize sticky table headers. function tableHeaderInitHandler(e) { var $tables = $(e.data.context).find('table.sticky-enabled').once('tableheader'); for (var i = 0, il = $tables.length; i < il; i++) { TableHeader.tables.push(new TableHeader($tables[i])); } } // Helper method to loop through tables and execute a method. function forTables(method, arg) { var tables = TableHeader.tables; for (var i = 0, il = tables.length; i < il; i++) { tables[i][method](arg); } } function tableHeaderResizeHandler(e) { forTables('recalculateSticky'); } function tableHeaderOnScrollHandler(e) { forTables('onScroll'); } function tableHeaderOffsetChangeHandler(e) { // Compute the new offset value. TableHeader.computeOffsetTop(); forTables('stickyPosition', TableHeader.offsetTop); } // Bind event that need to change all tables. $(window).on({ /** * When resizing table width and offset top can change, recalculate everything. */ 'resize.TableHeader': tableHeaderResizeHandler, /** * Bind only one event to take care of calling all scroll callbacks. */ 'scroll.TableHeader': tableHeaderOnScrollHandler }); // Bind to custom Drupal events. $(document).on({ /** * Recalculate columns width when window is resized and when show/hide * weight is triggered. */ 'columnschange.TableHeader': tableHeaderResizeHandler, /** * Offset value vas changed by a third party script. */ 'offsettopchange.TableHeader': tableHeaderOffsetChangeHandler }); /** * Constructor for the tableHeader object. Provides sticky table headers. * * TableHeader will make the current table header stick to the top of the page * if the table is very long. * * Fire a custom "topoffsetchange" event to make TableHeader compute the * new offset value from the "data-offset-top" attributes of relevant elements. * * @param table * DOM object for the table to add a sticky header to. * * @constructor */ function TableHeader(table) { var $table = $(table); this.$originalTable = $table; this.$originalHeader = $table.children('thead'); this.$originalHeaderCells = this.$originalHeader.find('> tr > th'); this.displayWeight = null; this.$originalTable.addClass('sticky-table'); this.tableHeight = $table[0].clientHeight; this.tableOffset = this.$originalTable.offset(); // React to columns change to avoid making checks in the scroll callback. this.$originalTable.bind('columnschange', {tableHeader: this}, function (e, display) { var tableHeader = e.data.tableHeader; if (tableHeader.displayWeight === null || tableHeader.displayWeight !== display) { tableHeader.recalculateSticky(); } tableHeader.displayWeight = display; }); // Create and display sticky header. this.createSticky(); } /** * Store the state of TableHeader. */ $.extend(TableHeader, { /** * This will store the state of all processed tables. * * @type {Array} */ tables: [], /** * Cache of computed offset value. * * @type {Number} */ offsetTop: 0, /** * Sum all [data-offset-top] values and cache it. */ computeOffsetTop: function () { var $offsets = $('[data-offset-top]'); var value, sum = 0; for (var i = 0, il = $offsets.length; i < il; i++) { value = parseInt($offsets[i].getAttribute('data-offset-top'), 10); sum += !isNaN(value) ? value : 0; } this.offsetTop = sum; return sum; } }); /** * Extend TableHeader prototype. */ $.extend(TableHeader.prototype, { /** * Minimum height in pixels for the table to have a sticky header. */ minHeight: 100, /** * Absolute position of the table on the page. */ tableOffset: null, /** * Absolute position of the table on the page. */ tableHeight: null, /** * Boolean storing the sticky header visibility state. */ stickyVisible: false, /** * Create the duplicate header. */ createSticky: function () { // Clone the table header so it inherits original jQuery properties. var $stickyHeader = this.$originalHeader.clone(true); // Hide the table to avoid a flash of the header clone upon page load. this.$stickyTable = $('') .css({ visibility: 'hidden', position: 'fixed', top: '0px' }) .append($stickyHeader) .insertBefore(this.$originalTable); this.$stickyHeaderCells = $stickyHeader.find('> tr > th'); // Initialize all computations. this.recalculateSticky(); }, /** * Set absolute position of sticky. * * @param offsetTop * @param offsetLeft */ stickyPosition: function (offsetTop, offsetLeft) { var css = {}; if (!isNaN(offsetTop)) { css.top = offsetTop + 'px'; } if (!isNaN(offsetLeft)) { css.left = (this.tableOffset.left - offsetLeft) + 'px'; } return this.$stickyTable.css(css); }, /** * Returns true if sticky is currently visible. */ checkStickyVisible: function () { var scrollTop = scrollValue('scrollTop'); var tableTop = this.tableOffset.top - TableHeader.offsetTop; var tableBottom = tableTop + this.tableHeight; var visible = false; if (tableTop < scrollTop && scrollTop < (tableBottom - this.minHeight)) { visible = true; } this.stickyVisible = visible; return visible; }, /** * Check if sticky header should be displayed. * * This function is throttled to once every 250ms to avoid unnecessary calls. * * @param event */ onScroll: function (e) { this.checkStickyVisible(); // Track horizontal positioning relative to the viewport. this.stickyPosition(null, scrollValue('scrollLeft')); this.$stickyTable.css('visibility', this.stickyVisible ? 'visible' : 'hidden'); }, /** * Event handler: recalculates position of the sticky table header. * * @param event * Event being triggered. */ recalculateSticky: function (event) { // Update table size. this.tableHeight = this.$originalTable[0].clientHeight; // Update offset top. TableHeader.computeOffsetTop(); this.tableOffset = this.$originalTable.offset(); this.stickyPosition(TableHeader.offsetTop); // Update columns width. var $that = null; var $stickyCell = null; var display = null; // Resize header and its cell widths. // Only apply width to visible table cells. This prevents the header from // displaying incorrectly when the sticky header is no longer visible. for (var i = 0, il = this.$originalHeaderCells.length; i < il; i++) { $that = $(this.$originalHeaderCells[i]); $stickyCell = this.$stickyHeaderCells.eq($that.index()); display = $that.css('display'); if (display !== 'none') { $stickyCell.css({'width': $that.css('width'), 'display': display}); } else { $stickyCell.css('display', 'none'); } } this.$stickyTable.css('width', this.$originalTable.outerWidth()); } }); // Expose constructor in the public space. Drupal.TableHeader = TableHeader; }(jQuery, Drupal));