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