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