X-Git-Url: http://www.aleph1.co.uk/gitweb/?p=yaffs-website;a=blobdiff_plain;f=web%2Fcore%2Fmisc%2Ftableheader.es6.js;fp=web%2Fcore%2Fmisc%2Ftableheader.es6.js;h=a5aa6635d34468c747f6e7ea9ef7c153e3f6cced;hp=0000000000000000000000000000000000000000;hb=9917807b03b64faf00f6a1f29dcb6eafc454efa5;hpb=aea91e65e895364e460983b890e295aa5d5540a5 diff --git a/web/core/misc/tableheader.es6.js b/web/core/misc/tableheader.es6.js new file mode 100644 index 000000000..a5aa6635d --- /dev/null +++ b/web/core/misc/tableheader.es6.js @@ -0,0 +1,312 @@ +/** + * @file + * Sticky table headers. + */ + +(function ($, Drupal, displace) { + /** + * 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); + }, + }; + + function scrollValue(position) { + return document.documentElement[position] || document.body[position]; + } + + // Select and initialize sticky table headers. + function tableHeaderInitHandler(e) { + const $tables = $(e.data.context).find('table.sticky-enabled').once('tableheader'); + const il = $tables.length; + for (let i = 0; i < il; i++) { + TableHeader.tables.push(new TableHeader($tables[i])); + } + forTables('onScroll'); + } + + // 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); + } + } + + function tableHeaderResizeHandler(e) { + forTables('recalculateSticky'); + } + + function tableHeaderOnScrollHandler(e) { + forTables('onScroll'); + } + + function tableHeaderOffsetChangeHandler(e, offsets) { + 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, + + /** + * Bind only one event to take care of calling all scroll callbacks. + * + * @ignore + */ + 'scroll.TableHeader': tableHeaderOnScrollHandler, + }); + // Bind to custom Drupal events. + $(document).on({ + + /** + * Recalculate columns width when window is resized and when show/hide + * weight is triggered. + * + * @ignore + */ + 'columnschange.TableHeader': tableHeaderResizeHandler, + + /** + * Recalculate TableHeader.topOffset when viewport is resized. + * + * @ignore + */ + 'drupalViewportOffsetChange.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. + * + * @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(); + + // 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; + }); + + // Create and display sticky header. + this.createSticky(); + } + + /** + * Store the state of TableHeader. + */ + $.extend(TableHeader, /** @lends Drupal.TableHeader */{ + + /** + * This will store the state of all processed tables. + * + * @type {Array.} + */ + tables: [], + }); + + /** + * 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, + + /** + * Absolute position of the table on the page. + * + * @type {?Drupal~displaceOffset} + */ + tableOffset: null, + + /** + * Absolute position of the table on the page. + * + * @type {?number} + */ + tableHeight: null, + + /** + * Boolean storing the sticky header visibility state. + * + * @type {bool} + */ + stickyVisible: false, + + /** + * Create the duplicate header. + */ + createSticky() { + // 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 = $('') + .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 {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`; + } + return this.$stickyTable.css(css); + }, + + /** + * 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.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.parent.Drupal.displace));