tableheader.es6.js 8.2 KB

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