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