rwd-table.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675
  1. /*!
  2. * Responsive Tables v5.0.4 (http://gergeo.se/RWD-Table-Patterns)
  3. * This is an awesome solution for responsive tables with complex data.
  4. * Authors: Nadan Gergeo <nadan.gergeo@gmail.com> (www.gergeo.se) & Maggie Wachs (www.filamentgroup.com)
  5. * Licensed under MIT (https://github.com/nadangergeo/RWD-Table-Patterns/blob/master/LICENSE-MIT)
  6. */
  7. (function ($) {
  8. 'use strict';
  9. // RESPONSIVE TABLE CLASS DEFINITION
  10. // ==========================
  11. var ResponsiveTable = function(element, options) {
  12. var that = this;
  13. this.options = options;
  14. this.$tableWrapper = null; //defined later in wrapTable
  15. this.$tableScrollWrapper = $(element); //defined later in wrapTable
  16. this.$table = $(element).find('.table-tool');
  17. if(this.$table.length !== 1) {
  18. return;
  19. throw new Error('Exactly one table is expected in a .table-tool-responsive div.');
  20. }
  21. //apply pattern option as data-attribute, in case it was set via js
  22. this.$tableScrollWrapper.attr('data-pattern', this.options.pattern);
  23. //if the table doesn't have a unique id, give it one.
  24. //The id will be a random hexadecimal value, prefixed with id.
  25. //Used for triggers with displayAll button.
  26. this.id = this.$table.prop('id') || this.$tableScrollWrapper.prop('id') || 'id' + Math.random().toString(16).slice(2);
  27. this.$tableClone = null; //defined farther down
  28. this.$stickyTableHeader = null; //defined farther down
  29. //good to have - for easy access
  30. this.$thead = this.$table.find('thead');
  31. this.$tbody = this.$table.find('tbody');
  32. this.$hdrCells = this.$thead.find('th');
  33. this.$bodyRows = this.$tbody.find('tr');
  34. //toolbar and buttons
  35. this.$btnToolbar = null; //defined farther down
  36. this.$dropdownGroup = null; //defined farther down
  37. this.$dropdownBtn = null; //defined farther down
  38. this.$dropdownContainer = null; //defined farther down
  39. this.$displayAllBtn = null; //defined farther down
  40. this.$focusGroup = null; //defined farther down
  41. this.$focusBtn = null; //defined farther down
  42. //misc
  43. this.displayAllTrigger = 'display-all-' + this.id + '.responsive-table';
  44. this.idPrefix = this.id + '-col-';
  45. // Check if iOS
  46. // property to save performance
  47. this.iOS = isIOS();
  48. // Setup table
  49. // -------------------------
  50. //wrap table
  51. this.wrapTable();
  52. //create toolbar with buttons
  53. this.createButtonToolbar();
  54. // Setup cells
  55. // -------------------------
  56. //setup header cells
  57. this.setupHdrCells();
  58. //setup standard cells
  59. this.setupStandardCells();
  60. //create sticky table head
  61. if(this.options.stickyTableHeader){
  62. this.createStickyTableHeader();
  63. }
  64. // hide toggle button if the list is empty
  65. if(this.$dropdownContainer.is(':empty')){
  66. this.$dropdownGroup.hide();
  67. }
  68. // Event binding
  69. // -------------------------
  70. // on orientchange, resize and displayAllBtn-click
  71. $(window).bind('orientationchange resize ' + this.displayAllTrigger, function(){
  72. //update the inputs' checked status
  73. that.$dropdownContainer.find('input').trigger('updateCheck');
  74. //update colspan and visibility of spanning cells
  75. $.proxy(that.updateSpanningCells(), that);
  76. });
  77. };
  78. ResponsiveTable.DEFAULTS = {
  79. pattern: 'priority-columns',
  80. stickyTableHeader: true,
  81. fixedNavbar: '.navbar-fixed-top', // Is there a fixed navbar? The stickyTableHeader needs to know about it!
  82. addDisplayAllBtn: true, // should it have a display-all button?
  83. addFocusBtn: true, // should it have a focus button?
  84. focusBtnIcon: 'glyphicon glyphicon-screenshot'
  85. };
  86. // Wrap table
  87. ResponsiveTable.prototype.wrapTable = function() {
  88. this.$tableScrollWrapper.wrap('<div class="table-wrapper"/>');
  89. this.$tableWrapper = this.$tableScrollWrapper.parent();
  90. };
  91. // Create toolbar with buttons
  92. ResponsiveTable.prototype.createButtonToolbar = function() {
  93. var that = this;
  94. this.$btnToolbar = $('<div class="btn-toolbar" />');
  95. this.$dropdownGroup = $('<div class="btn-group dropdown-btn-group pull-right" />');
  96. this.$dropdownBtn = $('<a class="btn btn-default dropdown-toggle" data-toggle="dropdown">Display <span class="caret"></span></a>');
  97. this.$dropdownContainer = $('<ul class="dropdown-menu"/>');
  98. // Focus btn
  99. if(this.options.addFocusBtn) {
  100. // Create focus btn group
  101. this.$focusGroup = $('<div class="btn-group focus-btn-group" />');
  102. // Create focus btn
  103. this.$focusBtn = $('<a class="btn btn-default">Focus</a>');
  104. if(this.options.focusBtnIcon) {
  105. this.$focusBtn.prepend('<span class="' + this.options.focusBtnIcon + '"></span> ');
  106. }
  107. // Add btn to group
  108. this.$focusGroup.append(this.$focusBtn);
  109. // Add focus btn to toolbar
  110. this.$btnToolbar.append(this.$focusGroup);
  111. // bind click on focus btn
  112. this.$focusBtn.click(function(){
  113. $.proxy(that.activateFocus(), that);
  114. });
  115. // bind click on rows
  116. this.$bodyRows.click(function(){
  117. $.proxy(that.focusOnRow($(this)), that);
  118. });
  119. }
  120. // Display-all btn
  121. if(this.options.addDisplayAllBtn) {
  122. // Create display-all btn
  123. this.$displayAllBtn = $('<a class="btn btn-default">Display all</a>');
  124. // Add display-all btn to dropdown-btn-group
  125. this.$dropdownGroup.append(this.$displayAllBtn);
  126. if (this.$table.hasClass('display-all')) {
  127. // add 'btn-primary' class to btn to indicate that display all is activated
  128. this.$displayAllBtn.addClass('btn-primary');
  129. }
  130. // bind click on display-all btn
  131. this.$displayAllBtn.click(function(){
  132. $.proxy(that.displayAll(null, true), that);
  133. });
  134. }
  135. //add dropdown btn and menu to dropdown-btn-group
  136. this.$dropdownGroup.append(this.$dropdownBtn).append(this.$dropdownContainer);
  137. //add dropdown group to toolbar
  138. this.$btnToolbar.append(this.$dropdownGroup);
  139. // add toolbar above table
  140. this.$tableScrollWrapper.before(this.$btnToolbar);
  141. };
  142. ResponsiveTable.prototype.clearAllFocus = function() {
  143. this.$bodyRows.removeClass('unfocused');
  144. this.$bodyRows.removeClass('focused');
  145. };
  146. ResponsiveTable.prototype.activateFocus = function() {
  147. // clear all
  148. this.clearAllFocus();
  149. if(this.$focusBtn){
  150. this.$focusBtn.toggleClass('btn-primary');
  151. }
  152. this.$table.toggleClass('focus-on');
  153. };
  154. ResponsiveTable.prototype.focusOnRow = function(row) {
  155. // only if activated (.i.e the table has the class focus-on)
  156. if(this.$table.hasClass('focus-on')) {
  157. var alreadyFocused = $(row).hasClass('focused');
  158. // clear all
  159. this.clearAllFocus();
  160. if(!alreadyFocused) {
  161. this.$bodyRows.addClass('unfocused');
  162. $(row).addClass('focused');
  163. }
  164. }
  165. };
  166. /**
  167. * @param activate Forces the displayAll to be active or not. If anything else than bool, it will not force the state so it will toggle as normal.
  168. * @param trigger Bool to indicate if the displayAllTrigger should be triggered.
  169. */
  170. ResponsiveTable.prototype.displayAll = function(activate, trigger) {
  171. if(this.$displayAllBtn){
  172. // add 'btn-primary' class to btn to indicate that display all is activated
  173. this.$displayAllBtn.toggleClass('btn-primary', activate);
  174. }
  175. this.$table.toggleClass('display-all', activate);
  176. if(this.$tableClone){
  177. this.$tableClone.toggleClass('display-all', activate);
  178. }
  179. if(trigger) {
  180. $(window).trigger(this.displayAllTrigger);
  181. }
  182. };
  183. ResponsiveTable.prototype.preserveDisplayAll = function() {
  184. var displayProp = 'table-cell';
  185. if($('html').hasClass('lt-ie9')){
  186. displayProp = 'inline';
  187. }
  188. $(this.$table).children('th, td').css('display', displayProp);
  189. if(this.$tableClone){
  190. $(this.$tableClone).children('th, td').css('display', displayProp);
  191. }
  192. };
  193. ResponsiveTable.prototype.createStickyTableHeader = function() {
  194. var that = this;
  195. //clone table head
  196. that.$tableClone = that.$table.clone();
  197. //replace ids
  198. that.$tableClone.prop('id', this.id + '-clone');
  199. that.$tableClone.find('[id]').each(function() {
  200. $(this).prop('id', $(this).prop('id') + '-clone');
  201. });
  202. // wrap table clone (this is our "sticky table header" now)
  203. that.$tableClone.wrap('<div class="sticky-table-header"/>');
  204. that.$stickyTableHeader = that.$tableClone.parent();
  205. // give the sticky table header same height as original
  206. that.$stickyTableHeader.css('height', that.$thead.height() + 2);
  207. //insert sticky table header
  208. if($('html').hasClass('lt-ie10')){
  209. that.$tableWrapper.prepend(that.$stickyTableHeader);
  210. } else {
  211. that.$table.before(that.$stickyTableHeader);
  212. }
  213. // var bodyRowsClone = $(tableClone).find('tbody').find('tr');
  214. // bind scroll and resize with updateStickyTableHeader
  215. $(window).bind('scroll resize', function(){
  216. $.proxy(that.updateStickyTableHeader(), that);
  217. });
  218. $(that.$tableScrollWrapper).bind('scroll', function(){
  219. $.proxy(that.updateStickyTableHeader(), that);
  220. });
  221. };
  222. // Help function for sticky table header
  223. ResponsiveTable.prototype.updateStickyTableHeader = function() {
  224. var that = this,
  225. top = 0,
  226. offsetTop = that.$table.offset().top,
  227. scrollTop = $(window).scrollTop() -1, //-1 to accomodate for top border
  228. maxTop = that.$table.height() - that.$stickyTableHeader.height(),
  229. rubberBandOffset = (scrollTop + $(window).height()) - $(document).height(),
  230. // useFixedSolution = that.$table.parent().prop('scrollWidth') === that.$table.parent().width();
  231. useFixedSolution = !that.iOS,
  232. navbarHeight = 0;
  233. //Is there a fixed navbar?
  234. if($(that.options.fixedNavbar).length) {
  235. var $navbar = $(that.options.fixedNavbar).first();
  236. navbarHeight = $navbar.height();
  237. scrollTop = scrollTop + navbarHeight;
  238. }
  239. var shouldBeVisible = (scrollTop > offsetTop) && (scrollTop < offsetTop + that.$table.height());
  240. if(useFixedSolution) {
  241. that.$stickyTableHeader.scrollLeft(that.$tableScrollWrapper.scrollLeft());
  242. //add fixedSolution class
  243. that.$stickyTableHeader.addClass('fixed-solution');
  244. // Calculate top property value (-1 to accomodate for top border)
  245. top = navbarHeight - 1;
  246. // When the about to scroll past the table, move sticky table head up
  247. if(((scrollTop - offsetTop) > maxTop)){
  248. top -= ((scrollTop - offsetTop) - maxTop);
  249. that.$stickyTableHeader.addClass('border-radius-fix');
  250. } else {
  251. that.$stickyTableHeader.removeClass('border-radius-fix');
  252. }
  253. if (shouldBeVisible) {
  254. //show sticky table header and update top and width.
  255. that.$stickyTableHeader.css({ 'visibility': 'visible', 'top': top + 'px', 'width': that.$tableScrollWrapper.innerWidth() + 'px'});
  256. //no more stuff to do - return!
  257. return;
  258. } else {
  259. //hide sticky table header and reset width
  260. that.$stickyTableHeader.css({'visibility': 'hidden', 'width': 'auto' });
  261. }
  262. } else { // alternate method
  263. //remove fixedSolution class
  264. that.$stickyTableHeader.removeClass('fixed-solution');
  265. //animation duration
  266. var animationDuration = 400;
  267. // Calculate top property value (-1 to accomodate for top border)
  268. top = scrollTop - offsetTop - 1;
  269. // Make sure the sticky table header doesn't slide up/down too far.
  270. if(top < 0) {
  271. top = 0;
  272. } else if (top > maxTop) {
  273. top = maxTop;
  274. }
  275. // Accomandate for rubber band effect
  276. if(rubberBandOffset > 0) {
  277. top = top - rubberBandOffset;
  278. }
  279. if (shouldBeVisible) {
  280. //show sticky table header (animate repositioning)
  281. that.$stickyTableHeader.css({ 'visibility': 'visible' });
  282. that.$stickyTableHeader.animate({ 'top': top + 'px' }, animationDuration);
  283. // hide original table head
  284. that.$thead.css({ 'visibility': 'hidden' });
  285. } else {
  286. that.$stickyTableHeader.animate({ 'top': '0' }, animationDuration, function(){
  287. // show original table head
  288. that.$thead.css({ 'visibility': 'visible' });
  289. // hide sticky table head
  290. that.$stickyTableHeader.css({ 'visibility': 'hidden' });
  291. });
  292. }
  293. }
  294. };
  295. // Setup header cells
  296. ResponsiveTable.prototype.setupHdrCells = function() {
  297. var that = this;
  298. // for each header column
  299. that.$hdrCells.each(function(i){
  300. var $th = $(this),
  301. id = $th.prop('id'),
  302. thText = $th.text();
  303. // assign an id to each header, if none is in the markup
  304. if (!id) {
  305. id = that.idPrefix + i;
  306. $th.prop('id', id);
  307. }
  308. if(thText === ''){
  309. thText = $th.attr('data-col-name');
  310. }
  311. // create the hide/show toggle for the current column
  312. if ( $th.is('[data-priority]') ) {
  313. var $toggle = $('<li class="checkbox-row"><input type="checkbox" name="toggle-'+id+'" id="toggle-'+id+'" value="'+id+'" /> <label for="toggle-'+id+'">'+ thText +'</label></li>');
  314. var $checkbox = $toggle.find('input');
  315. that.$dropdownContainer.append($toggle);
  316. $toggle.click(function(){
  317. // console.log("cliiiick!");
  318. $checkbox.prop('checked', !$checkbox.prop('checked'));
  319. $checkbox.trigger('change');
  320. });
  321. //Freakin' IE fix
  322. if ($('html').hasClass('lt-ie9')) {
  323. $checkbox.click(function() {
  324. $(this).trigger('change');
  325. });
  326. }
  327. $toggle.find('label').click(function(event){
  328. event.stopPropagation();
  329. });
  330. $toggle.find('input')
  331. .click(function(event){
  332. event.stopPropagation();
  333. })
  334. .change(function(){ // bind change event on checkbox
  335. var $checkbox = $(this),
  336. val = $checkbox.val(),
  337. //all cells under the column, including the header and its clone
  338. $cells = that.$tableWrapper.find('#' + val + ', #' + val + '-clone, [data-columns~='+ val +']');
  339. //if display-all is on - save state and carry on
  340. if(that.$table.hasClass('display-all')){
  341. //save state
  342. $.proxy(that.preserveDisplayAll(), that);
  343. //remove display all class
  344. that.$table.removeClass('display-all');
  345. if(that.$tableClone){
  346. that.$tableClone.removeClass('display-all');
  347. }
  348. //switch off button
  349. that.$displayAllBtn.removeClass('btn-primary');
  350. }
  351. // loop through the cells
  352. $cells.each(function(){
  353. var $cell = $(this);
  354. // is the checkbox checked now?
  355. if ($checkbox.is(':checked')) {
  356. // if the cell was already visible, it means its original colspan was >1
  357. // so let's increment the colspan
  358. if($cell.css('display') !== 'none'){
  359. $cell.prop('colSpan', parseInt($cell.prop('colSpan')) + 1);
  360. }
  361. // show cell
  362. $cell.show();
  363. }
  364. // checkbox has been unchecked
  365. else {
  366. // decrement colSpan if it's not 1 (because colSpan should not be 0)
  367. if(parseInt($cell.prop('colSpan'))>1){
  368. $cell.prop('colSpan', parseInt($cell.prop('colSpan')) - 1);
  369. }
  370. // otherwise, hide the cell
  371. else {
  372. $cell.hide();
  373. }
  374. }
  375. });
  376. })
  377. .bind('updateCheck', function(){
  378. if ( $th.css('display') !== 'none') {
  379. $(this).prop('checked', true);
  380. }
  381. else {
  382. $(this).prop('checked', false);
  383. }
  384. })
  385. .trigger('updateCheck');
  386. } // end if
  387. }); // end hdrCells loop
  388. };
  389. // Setup standard cells
  390. // assign matching "data-columns" attributes to the associated cells "(cells with colspan>1 has multiple columns).
  391. ResponsiveTable.prototype.setupStandardCells = function() {
  392. var that = this;
  393. // for each body rows
  394. that.$bodyRows.each(function(){
  395. var idStart = 0;
  396. // for each cell
  397. $(this).children('th, td').each(function(){
  398. var $cell = $(this);
  399. var columnsAttr = '';
  400. var colSpan = $cell.prop('colSpan');
  401. var numOfHidden = 0;
  402. // loop through columns that the cell spans over
  403. for (var k = idStart; k < (idStart + colSpan); k++) {
  404. // add column id
  405. columnsAttr = columnsAttr + ' ' + that.idPrefix + k;
  406. // get column header
  407. var $colHdr = that.$tableScrollWrapper.find('#' + that.idPrefix + k);
  408. // copy data-priority attribute from column header
  409. var dataPriority = $colHdr.attr('data-priority');
  410. if (dataPriority) { $cell.attr('data-priority', dataPriority); }
  411. if($colHdr.css('display')==='none'){
  412. numOfHidden++;
  413. }
  414. }
  415. // if colSpan is more than 1
  416. if(colSpan > 1) {
  417. //give it the class 'spn-cell';
  418. $cell.addClass('spn-cell');
  419. // if one of the columns that the cell belongs to is visible then show the cell
  420. if(numOfHidden !== colSpan){
  421. $cell.show();
  422. } else {
  423. $cell.hide(); //just in case
  424. }
  425. }
  426. //update colSpan to match number of visible columns that i belongs to
  427. $cell.prop('colSpan',Math.max((colSpan - numOfHidden),1));
  428. //remove whitespace in begining of string.
  429. columnsAttr = columnsAttr.substring(1);
  430. //set attribute to cell
  431. $cell.attr('data-columns', columnsAttr);
  432. //increment idStart with the current cells colSpan.
  433. idStart = idStart + colSpan;
  434. });
  435. });
  436. };
  437. // Update colspan and visibility of spanning cells
  438. ResponsiveTable.prototype.updateSpanningCells = function() {
  439. var that = this;
  440. // iterate through cells with class 'spn-cell'
  441. that.$table.find('.spn-cell').each( function(){
  442. var $cell = $(this);
  443. var columnsAttr = $cell.attr('data-columns').split(' ');
  444. var colSpan = columnsAttr.length;
  445. var numOfHidden = 0;
  446. for (var i = 0; i < colSpan; i++) {
  447. if($('#' + columnsAttr[i]).css('display')==='none'){
  448. numOfHidden++;
  449. }
  450. }
  451. // if one of the columns that the cell belongs to is visible then show the cell
  452. if(numOfHidden !== colSpan){
  453. $cell.show();
  454. } else {
  455. $cell.hide(); //just in case
  456. }
  457. // console.log('numOfHidden: ' + numOfHidden);
  458. // console.log("new colSpan:" +Math.max((colSpan - numOfHidden),1));
  459. //update colSpan to match number of visible columns that i belongs to
  460. $cell.prop('colSpan',Math.max((colSpan - numOfHidden),1));
  461. });
  462. };
  463. // RESPONSIVE TABLE PLUGIN DEFINITION
  464. // ===========================
  465. var old = $.fn.responsiveTable;
  466. $.fn.responsiveTable = function (option) {
  467. return this.each(function () {
  468. var $this = $(this);
  469. var data = $this.data('responsiveTable');
  470. var options = $.extend({}, ResponsiveTable.DEFAULTS, $this.data(), typeof option === 'object' && option);
  471. if(options.pattern === '') {
  472. return;
  473. }
  474. if (!data) {
  475. $this.data('responsiveTable', (data = new ResponsiveTable(this, options)));
  476. }
  477. if (typeof option === 'string') {
  478. data[option]();
  479. }
  480. });
  481. };
  482. $.fn.responsiveTable.Constructor = ResponsiveTable;
  483. // RESPONSIVE TABLE NO CONFLICT
  484. // =====================
  485. $.fn.responsiveTable.noConflict = function () {
  486. $.fn.responsiveTable = old;
  487. return this;
  488. };
  489. // RESPONSIVE TABLE DATA-API
  490. // ==================
  491. $(document).on('ready.responsive-table.data-api', function () {
  492. $('[data-pattern]').each(function () {
  493. var $tableScrollWrapper = $(this);
  494. $tableScrollWrapper.responsiveTable($tableScrollWrapper.data());
  495. });
  496. });
  497. // DROPDOWN
  498. // ==========================
  499. // Prevent dropdown from closing when toggling checkbox
  500. $(document).on('click.dropdown.data-api', '.dropdown-menu .checkbox-row', function (e) {
  501. e.stopPropagation();
  502. });
  503. // FEATURE DETECTION (instead of Modernizr)
  504. // ==========================
  505. // media queries
  506. function mediaQueriesSupported() {
  507. return (typeof window.matchMedia !== 'undefined' || typeof window.msMatchMedia !== 'undefined' || typeof window.styleMedia !== 'undefined');
  508. }
  509. // touch
  510. function hasTouch() {
  511. return 'ontouchstart' in window;
  512. }
  513. // Checks if current browser is on IOS.
  514. function isIOS() {
  515. return !!(navigator.userAgent.match(/iPhone/i) || navigator.userAgent.match(/iPad/i) || navigator.userAgent.match(/iPod/i));
  516. }
  517. $(document).ready(function() {
  518. // Change `no-js` to `js`
  519. $('html').removeClass('no-js').addClass('js');
  520. // Add mq/no-mq class to html
  521. if(mediaQueriesSupported()) {
  522. $('html').addClass('mq');
  523. } else {
  524. $('html').addClass('no-mq');
  525. }
  526. // Add touch/no-touch class to html
  527. if(hasTouch()) {
  528. $('html').addClass('touch');
  529. } else {
  530. $('html').addClass('no-touch');
  531. }
  532. });
  533. })(jQuery);