diff options
Diffstat (limited to 'core/misc/tableheader.js')
-rw-r--r-- | core/misc/tableheader.js | 366 |
1 files changed, 267 insertions, 99 deletions
diff --git a/core/misc/tableheader.js b/core/misc/tableheader.js index 35eef305737..1d4eca7e69e 100644 --- a/core/misc/tableheader.js +++ b/core/misc/tableheader.js @@ -1,57 +1,105 @@ /** -* DO NOT EDIT THIS FILE. -* See the following change record for more information, -* https://www.drupal.org/node/2815083 -* @preserve -**/ + * @file + * Sticky table headers. + */ (function ($, Drupal, displace) { + /** + * 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. + * + * @constructor Drupal.TableHeader + * + * @param {HTMLElement} table + * DOM object for the table to add a sticky header to. + * + * @listens event:columnschange + */ function TableHeader(table) { const $table = $(table); + + /** + * @name Drupal.TableHeader#$originalTable + * + * @type {HTMLElement} + */ this.$originalTable = $table; + + /** + * @type {jQuery} + */ this.$originalHeader = $table.children('thead'); + + /** + * @type {jQuery} + */ this.$originalHeaderCells = this.$originalHeader.find('> tr > th'); + + /** + * @type {null|bool} + */ this.displayWeight = null; this.$originalTable.addClass('sticky-table'); this.tableHeight = $table[0].clientHeight; this.tableOffset = this.$originalTable.offset(); - this.$originalTable.on('columnschange', { - tableHeader: this - }, (e, display) => { - const tableHeader = e.data.tableHeader; - if (tableHeader.displayWeight === null || tableHeader.displayWeight !== display) { - tableHeader.recalculateSticky(); - } + // React to columns change to avoid making checks in the scroll callback. + this.$originalTable.on( + 'columnschange', + { tableHeader: this }, + (e, display) => { + const tableHeader = e.data.tableHeader; + if ( + tableHeader.displayWeight === null || + tableHeader.displayWeight !== display + ) { + tableHeader.recalculateSticky(); + } + tableHeader.displayWeight = display; + }, + ); - tableHeader.displayWeight = display; - }); + // Create and display sticky header. this.createSticky(); } + // Helper method to loop through tables and execute a method. function forTables(method, arg) { const tables = TableHeader.tables; const il = tables.length; - for (let i = 0; i < il; i++) { tables[i][method](arg); } } + // Select and initialize sticky table headers. function tableHeaderInitHandler(e) { - once('tableheader', $(e.data.context).find('table.sticky-enabled')).forEach(table => { - TableHeader.tables.push(new TableHeader(table)); - }); + once('tableheader', $(e.data.context).find('table.sticky-enabled')).forEach( + (table) => { + TableHeader.tables.push(new TableHeader(table)); + }, + ); forTables('onScroll'); } + /** + * Attaches sticky table headers. + * + * @type {Drupal~behavior} + * + * @prop {Drupal~behaviorAttach} attach + * Attaches the sticky table header behavior. + */ Drupal.behaviors.tableHeader = { attach(context) { - $(window).one('scroll.TableHeaderInit', { - context - }, tableHeaderInitHandler); - } - + $(window).one( + 'scroll.TableHeaderInit', + { context }, + tableHeaderInitHandler, + ); + }, }; function scrollValue(position) { @@ -70,98 +118,218 @@ forTables('stickyPosition', offsets.top); } + // Bind event that need to change all tables. $(window).on({ + /** + * When resizing table width can change, recalculate everything. + * + * @ignore + */ 'resize.TableHeader': tableHeaderResizeHandler, - 'scroll.TableHeader': tableHeaderOnScrollHandler + + /** + * Bind only one event to take care of calling all scroll callbacks. + * + * @ignore + */ + 'scroll.TableHeader': tableHeaderOnScrollHandler, }); + // Bind to custom Drupal events. $(document).on({ - 'columnschange.TableHeader drupalToolbarTrayChange': tableHeaderResizeHandler, - 'drupalViewportOffsetChange.TableHeader': tableHeaderOffsetChangeHandler - }); - $.extend(TableHeader, { - tables: [] + /** + * Recalculate columns width when window is resized, when show/hide weight + * is triggered, or when toolbar tray is toggled. + * + * @ignore + */ + 'columnschange.TableHeader drupalToolbarTrayChange': + tableHeaderResizeHandler, + + /** + * Recalculate TableHeader.topOffset when viewport is resized. + * + * @ignore + */ + 'drupalViewportOffsetChange.TableHeader': tableHeaderOffsetChangeHandler, }); - $.extend(TableHeader.prototype, { - minHeight: 100, - tableOffset: null, - tableHeight: null, - stickyVisible: false, - - createSticky() { - this.$html = $('html'); - const $stickyHeader = this.$originalHeader.clone(true); - this.$stickyTable = $('<table class="sticky-header"></table>').css({ - visibility: 'hidden', - position: 'fixed', - top: '0px' - }).append($stickyHeader).insertBefore(this.$originalTable); - this.$stickyHeaderCells = $stickyHeader.find('> tr > th'); - this.recalculateSticky(); + + /** + * Store the state of TableHeader. + */ + $.extend( + TableHeader, + /** @lends Drupal.TableHeader */ { + /** + * This will store the state of all processed tables. + * + * @type {Array.<Drupal.TableHeader>} + */ + tables: [], }, + ); - stickyPosition(offsetTop, offsetLeft) { - const css = {}; + /** + * Extend TableHeader prototype. + */ + $.extend( + TableHeader.prototype, + /** @lends Drupal.TableHeader# */ { + /** + * Minimum height in pixels for the table to have a sticky header. + * + * @type {number} + */ + minHeight: 100, - if (typeof offsetTop === 'number') { - css.top = `${offsetTop}px`; - } + /** + * Absolute position of the table on the page. + * + * @type {?Drupal~displaceOffset} + */ + tableOffset: null, - if (typeof offsetLeft === 'number') { - css.left = `${this.tableOffset.left - offsetLeft}px`; - } + /** + * Absolute position of the table on the page. + * + * @type {?number} + */ + tableHeight: null, - this.$html.css('scroll-padding-top', displace.offsets.top + (this.stickyVisible ? this.$stickyTable.height() : 0)); - return this.$stickyTable.css(css); - }, + /** + * Boolean storing the sticky header visibility state. + * + * @type {bool} + */ + stickyVisible: false, - checkStickyVisible() { - const scrollTop = scrollValue('scrollTop'); - const tableTop = this.tableOffset.top - displace.offsets.top; - const tableBottom = tableTop + this.tableHeight; - let visible = false; + /** + * Create the duplicate header. + */ + createSticky() { + // For caching purposes. + this.$html = $('html'); + // Clone the table header so it inherits original jQuery properties. + const $stickyHeader = this.$originalHeader.clone(true); + // Hide the table to avoid a flash of the header clone upon page load. + this.$stickyTable = $('<table class="sticky-header"></table>') + .css({ + visibility: 'hidden', + position: 'fixed', + top: '0px', + }) + .append($stickyHeader) + .insertBefore(this.$originalTable); - if (tableTop < scrollTop && scrollTop < tableBottom - this.minHeight) { - visible = true; - } + this.$stickyHeaderCells = $stickyHeader.find('> tr > th'); - this.stickyVisible = visible; - return visible; - }, + // Initialize all computations. + this.recalculateSticky(); + }, - onScroll(e) { - this.checkStickyVisible(); - this.stickyPosition(null, scrollValue('scrollLeft')); - this.$stickyTable.css('visibility', this.stickyVisible ? 'visible' : 'hidden'); - }, + /** + * Set absolute position of sticky. + * + * @param {number} offsetTop + * The top offset for the sticky header. + * @param {number} offsetLeft + * The left offset for the sticky header. + * + * @return {jQuery} + * The sticky table as a jQuery collection. + */ + stickyPosition(offsetTop, offsetLeft) { + const css = {}; + if (typeof offsetTop === 'number') { + css.top = `${offsetTop}px`; + } + if (typeof offsetLeft === 'number') { + css.left = `${this.tableOffset.left - offsetLeft}px`; + } + this.$html.css( + 'scroll-padding-top', + displace.offsets.top + + (this.stickyVisible ? this.$stickyTable.height() : 0), + ); + return this.$stickyTable.css(css); + }, - recalculateSticky(event) { - this.tableHeight = this.$originalTable[0].clientHeight; - displace.offsets.top = displace.calculateOffset('top'); - this.tableOffset = this.$originalTable.offset(); - this.stickyPosition(displace.offsets.top, scrollValue('scrollLeft')); - let $that = null; - let $stickyCell = null; - let display = null; - const il = this.$originalHeaderCells.length; - - for (let i = 0; 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 - }); - } else { - $stickyCell.css('display', 'none'); + /** + * Returns true if sticky is currently visible. + * + * @return {bool} + * The visibility status. + */ + checkStickyVisible() { + const scrollTop = scrollValue('scrollTop'); + const tableTop = this.tableOffset.top - displace.offsets.top; + const tableBottom = tableTop + this.tableHeight; + let visible = false; + + if (tableTop < scrollTop && scrollTop < tableBottom - this.minHeight) { + visible = true; } - } - this.$stickyTable.css('width', this.$originalTable.outerWidth()); - } + 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 {jQuery.Event} e + * The scroll event. + */ + onScroll(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 {jQuery.Event} event + * Event being triggered. + */ + recalculateSticky(event) { + // Update table size. + this.tableHeight = this.$originalTable[0].clientHeight; + + // Update offset top. + displace.offsets.top = displace.calculateOffset('top'); + this.tableOffset = this.$originalTable.offset(); + this.stickyPosition(displace.offsets.top, scrollValue('scrollLeft')); + + // Update columns width. + let $that = null; + let $stickyCell = null; + let 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. + const il = this.$originalHeaderCells.length; + for (let i = 0; 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 }); + } else { + $stickyCell.css('display', 'none'); + } + } + this.$stickyTable.css('width', this.$originalTable.outerWidth()); + }, + }, + ); + + // Expose constructor in the public space. Drupal.TableHeader = TableHeader; -})(jQuery, Drupal, window.Drupal.displace);
\ No newline at end of file +})(jQuery, Drupal, window.Drupal.displace); |