annotate core/misc/tableheader.es6.js @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents 129ea1e6d783
children
rev   line source
Chris@0 1 /**
Chris@0 2 * @file
Chris@0 3 * Sticky table headers.
Chris@0 4 */
Chris@0 5
Chris@17 6 (function($, Drupal, displace) {
Chris@0 7 /**
Chris@0 8 * Constructor for the tableHeader object. Provides sticky table headers.
Chris@0 9 *
Chris@0 10 * TableHeader will make the current table header stick to the top of the page
Chris@0 11 * if the table is very long.
Chris@0 12 *
Chris@0 13 * @constructor Drupal.TableHeader
Chris@0 14 *
Chris@0 15 * @param {HTMLElement} table
Chris@0 16 * DOM object for the table to add a sticky header to.
Chris@0 17 *
Chris@0 18 * @listens event:columnschange
Chris@0 19 */
Chris@0 20 function TableHeader(table) {
Chris@0 21 const $table = $(table);
Chris@0 22
Chris@0 23 /**
Chris@0 24 * @name Drupal.TableHeader#$originalTable
Chris@0 25 *
Chris@0 26 * @type {HTMLElement}
Chris@0 27 */
Chris@0 28 this.$originalTable = $table;
Chris@0 29
Chris@0 30 /**
Chris@0 31 * @type {jQuery}
Chris@0 32 */
Chris@0 33 this.$originalHeader = $table.children('thead');
Chris@0 34
Chris@0 35 /**
Chris@0 36 * @type {jQuery}
Chris@0 37 */
Chris@0 38 this.$originalHeaderCells = this.$originalHeader.find('> tr > th');
Chris@0 39
Chris@0 40 /**
Chris@0 41 * @type {null|bool}
Chris@0 42 */
Chris@0 43 this.displayWeight = null;
Chris@0 44 this.$originalTable.addClass('sticky-table');
Chris@0 45 this.tableHeight = $table[0].clientHeight;
Chris@0 46 this.tableOffset = this.$originalTable.offset();
Chris@0 47
Chris@0 48 // React to columns change to avoid making checks in the scroll callback.
Chris@17 49 this.$originalTable.on(
Chris@17 50 'columnschange',
Chris@17 51 { tableHeader: this },
Chris@17 52 (e, display) => {
Chris@17 53 const tableHeader = e.data.tableHeader;
Chris@17 54 if (
Chris@17 55 tableHeader.displayWeight === null ||
Chris@17 56 tableHeader.displayWeight !== display
Chris@17 57 ) {
Chris@17 58 tableHeader.recalculateSticky();
Chris@17 59 }
Chris@17 60 tableHeader.displayWeight = display;
Chris@17 61 },
Chris@17 62 );
Chris@0 63
Chris@0 64 // Create and display sticky header.
Chris@0 65 this.createSticky();
Chris@0 66 }
Chris@0 67
Chris@17 68 // Helper method to loop through tables and execute a method.
Chris@17 69 function forTables(method, arg) {
Chris@17 70 const tables = TableHeader.tables;
Chris@17 71 const il = tables.length;
Chris@17 72 for (let i = 0; i < il; i++) {
Chris@17 73 tables[i][method](arg);
Chris@17 74 }
Chris@17 75 }
Chris@17 76
Chris@17 77 // Select and initialize sticky table headers.
Chris@17 78 function tableHeaderInitHandler(e) {
Chris@17 79 const $tables = $(e.data.context)
Chris@17 80 .find('table.sticky-enabled')
Chris@17 81 .once('tableheader');
Chris@17 82 const il = $tables.length;
Chris@17 83 for (let i = 0; i < il; i++) {
Chris@17 84 TableHeader.tables.push(new TableHeader($tables[i]));
Chris@17 85 }
Chris@17 86 forTables('onScroll');
Chris@17 87 }
Chris@17 88
Chris@17 89 /**
Chris@17 90 * Attaches sticky table headers.
Chris@17 91 *
Chris@17 92 * @type {Drupal~behavior}
Chris@17 93 *
Chris@17 94 * @prop {Drupal~behaviorAttach} attach
Chris@17 95 * Attaches the sticky table header behavior.
Chris@17 96 */
Chris@17 97 Drupal.behaviors.tableHeader = {
Chris@17 98 attach(context) {
Chris@17 99 $(window).one(
Chris@17 100 'scroll.TableHeaderInit',
Chris@17 101 { context },
Chris@17 102 tableHeaderInitHandler,
Chris@17 103 );
Chris@17 104 },
Chris@17 105 };
Chris@17 106
Chris@17 107 function scrollValue(position) {
Chris@17 108 return document.documentElement[position] || document.body[position];
Chris@17 109 }
Chris@17 110
Chris@17 111 function tableHeaderResizeHandler(e) {
Chris@17 112 forTables('recalculateSticky');
Chris@17 113 }
Chris@17 114
Chris@17 115 function tableHeaderOnScrollHandler(e) {
Chris@17 116 forTables('onScroll');
Chris@17 117 }
Chris@17 118
Chris@17 119 function tableHeaderOffsetChangeHandler(e, offsets) {
Chris@17 120 forTables('stickyPosition', offsets.top);
Chris@17 121 }
Chris@17 122
Chris@17 123 // Bind event that need to change all tables.
Chris@17 124 $(window).on({
Chris@17 125 /**
Chris@17 126 * When resizing table width can change, recalculate everything.
Chris@17 127 *
Chris@17 128 * @ignore
Chris@17 129 */
Chris@17 130 'resize.TableHeader': tableHeaderResizeHandler,
Chris@17 131
Chris@17 132 /**
Chris@17 133 * Bind only one event to take care of calling all scroll callbacks.
Chris@17 134 *
Chris@17 135 * @ignore
Chris@17 136 */
Chris@17 137 'scroll.TableHeader': tableHeaderOnScrollHandler,
Chris@17 138 });
Chris@17 139 // Bind to custom Drupal events.
Chris@17 140 $(document).on({
Chris@17 141 /**
Chris@17 142 * Recalculate columns width when window is resized and when show/hide
Chris@17 143 * weight is triggered.
Chris@17 144 *
Chris@17 145 * @ignore
Chris@17 146 */
Chris@17 147 'columnschange.TableHeader': tableHeaderResizeHandler,
Chris@17 148
Chris@17 149 /**
Chris@17 150 * Recalculate TableHeader.topOffset when viewport is resized.
Chris@17 151 *
Chris@17 152 * @ignore
Chris@17 153 */
Chris@17 154 'drupalViewportOffsetChange.TableHeader': tableHeaderOffsetChangeHandler,
Chris@17 155 });
Chris@17 156
Chris@0 157 /**
Chris@0 158 * Store the state of TableHeader.
Chris@0 159 */
Chris@17 160 $.extend(
Chris@17 161 TableHeader,
Chris@17 162 /** @lends Drupal.TableHeader */ {
Chris@17 163 /**
Chris@17 164 * This will store the state of all processed tables.
Chris@17 165 *
Chris@17 166 * @type {Array.<Drupal.TableHeader>}
Chris@17 167 */
Chris@17 168 tables: [],
Chris@17 169 },
Chris@17 170 );
Chris@0 171
Chris@0 172 /**
Chris@0 173 * Extend TableHeader prototype.
Chris@0 174 */
Chris@17 175 $.extend(
Chris@17 176 TableHeader.prototype,
Chris@17 177 /** @lends Drupal.TableHeader# */ {
Chris@17 178 /**
Chris@17 179 * Minimum height in pixels for the table to have a sticky header.
Chris@17 180 *
Chris@17 181 * @type {number}
Chris@17 182 */
Chris@17 183 minHeight: 100,
Chris@0 184
Chris@17 185 /**
Chris@17 186 * Absolute position of the table on the page.
Chris@17 187 *
Chris@17 188 * @type {?Drupal~displaceOffset}
Chris@17 189 */
Chris@17 190 tableOffset: null,
Chris@0 191
Chris@17 192 /**
Chris@17 193 * Absolute position of the table on the page.
Chris@17 194 *
Chris@17 195 * @type {?number}
Chris@17 196 */
Chris@17 197 tableHeight: null,
Chris@0 198
Chris@17 199 /**
Chris@17 200 * Boolean storing the sticky header visibility state.
Chris@17 201 *
Chris@17 202 * @type {bool}
Chris@17 203 */
Chris@17 204 stickyVisible: false,
Chris@0 205
Chris@17 206 /**
Chris@17 207 * Create the duplicate header.
Chris@17 208 */
Chris@17 209 createSticky() {
Chris@17 210 // Clone the table header so it inherits original jQuery properties.
Chris@17 211 const $stickyHeader = this.$originalHeader.clone(true);
Chris@17 212 // Hide the table to avoid a flash of the header clone upon page load.
Chris@17 213 this.$stickyTable = $('<table class="sticky-header"/>')
Chris@17 214 .css({
Chris@17 215 visibility: 'hidden',
Chris@17 216 position: 'fixed',
Chris@17 217 top: '0px',
Chris@17 218 })
Chris@17 219 .append($stickyHeader)
Chris@17 220 .insertBefore(this.$originalTable);
Chris@0 221
Chris@17 222 this.$stickyHeaderCells = $stickyHeader.find('> tr > th');
Chris@0 223
Chris@17 224 // Initialize all computations.
Chris@17 225 this.recalculateSticky();
Chris@17 226 },
Chris@0 227
Chris@17 228 /**
Chris@17 229 * Set absolute position of sticky.
Chris@17 230 *
Chris@17 231 * @param {number} offsetTop
Chris@17 232 * The top offset for the sticky header.
Chris@17 233 * @param {number} offsetLeft
Chris@17 234 * The left offset for the sticky header.
Chris@17 235 *
Chris@17 236 * @return {jQuery}
Chris@17 237 * The sticky table as a jQuery collection.
Chris@17 238 */
Chris@17 239 stickyPosition(offsetTop, offsetLeft) {
Chris@17 240 const css = {};
Chris@17 241 if (typeof offsetTop === 'number') {
Chris@17 242 css.top = `${offsetTop}px`;
Chris@17 243 }
Chris@17 244 if (typeof offsetLeft === 'number') {
Chris@17 245 css.left = `${this.tableOffset.left - offsetLeft}px`;
Chris@17 246 }
Chris@17 247 return this.$stickyTable.css(css);
Chris@17 248 },
Chris@17 249
Chris@17 250 /**
Chris@17 251 * Returns true if sticky is currently visible.
Chris@17 252 *
Chris@17 253 * @return {bool}
Chris@17 254 * The visibility status.
Chris@17 255 */
Chris@17 256 checkStickyVisible() {
Chris@17 257 const scrollTop = scrollValue('scrollTop');
Chris@17 258 const tableTop = this.tableOffset.top - displace.offsets.top;
Chris@17 259 const tableBottom = tableTop + this.tableHeight;
Chris@17 260 let visible = false;
Chris@17 261
Chris@17 262 if (tableTop < scrollTop && scrollTop < tableBottom - this.minHeight) {
Chris@17 263 visible = true;
Chris@17 264 }
Chris@17 265
Chris@17 266 this.stickyVisible = visible;
Chris@17 267 return visible;
Chris@17 268 },
Chris@17 269
Chris@17 270 /**
Chris@17 271 * Check if sticky header should be displayed.
Chris@17 272 *
Chris@17 273 * This function is throttled to once every 250ms to avoid unnecessary
Chris@17 274 * calls.
Chris@17 275 *
Chris@17 276 * @param {jQuery.Event} e
Chris@17 277 * The scroll event.
Chris@17 278 */
Chris@17 279 onScroll(e) {
Chris@17 280 this.checkStickyVisible();
Chris@17 281 // Track horizontal positioning relative to the viewport.
Chris@17 282 this.stickyPosition(null, scrollValue('scrollLeft'));
Chris@17 283 this.$stickyTable.css(
Chris@17 284 'visibility',
Chris@17 285 this.stickyVisible ? 'visible' : 'hidden',
Chris@17 286 );
Chris@17 287 },
Chris@17 288
Chris@17 289 /**
Chris@17 290 * Event handler: recalculates position of the sticky table header.
Chris@17 291 *
Chris@17 292 * @param {jQuery.Event} event
Chris@17 293 * Event being triggered.
Chris@17 294 */
Chris@17 295 recalculateSticky(event) {
Chris@17 296 // Update table size.
Chris@17 297 this.tableHeight = this.$originalTable[0].clientHeight;
Chris@17 298
Chris@17 299 // Update offset top.
Chris@17 300 displace.offsets.top = displace.calculateOffset('top');
Chris@17 301 this.tableOffset = this.$originalTable.offset();
Chris@17 302 this.stickyPosition(displace.offsets.top, scrollValue('scrollLeft'));
Chris@17 303
Chris@17 304 // Update columns width.
Chris@17 305 let $that = null;
Chris@17 306 let $stickyCell = null;
Chris@17 307 let display = null;
Chris@17 308 // Resize header and its cell widths.
Chris@17 309 // Only apply width to visible table cells. This prevents the header from
Chris@17 310 // displaying incorrectly when the sticky header is no longer visible.
Chris@17 311 const il = this.$originalHeaderCells.length;
Chris@17 312 for (let i = 0; i < il; i++) {
Chris@17 313 $that = $(this.$originalHeaderCells[i]);
Chris@17 314 $stickyCell = this.$stickyHeaderCells.eq($that.index());
Chris@17 315 display = $that.css('display');
Chris@17 316 if (display !== 'none') {
Chris@17 317 $stickyCell.css({ width: $that.css('width'), display });
Chris@17 318 } else {
Chris@17 319 $stickyCell.css('display', 'none');
Chris@17 320 }
Chris@17 321 }
Chris@17 322 this.$stickyTable.css('width', this.$originalTable.outerWidth());
Chris@17 323 },
Chris@0 324 },
Chris@17 325 );
Chris@0 326
Chris@0 327 // Expose constructor in the public space.
Chris@0 328 Drupal.TableHeader = TableHeader;
Chris@17 329 })(jQuery, Drupal, window.Drupal.displace);