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);
|