tableheader.es6.js 8.7 KB

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