tableheader.js 8.3 KB

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