dataTables.responsive.js 38 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393
  1. /*! Responsive 2.2.2
  2. * 2014-2018 SpryMedia Ltd - datatables.net/license
  3. */
  4. /**
  5. * @summary Responsive
  6. * @description Responsive tables plug-in for DataTables
  7. * @version 2.2.2
  8. * @file dataTables.responsive.js
  9. * @author SpryMedia Ltd (www.sprymedia.co.uk)
  10. * @contact www.sprymedia.co.uk/contact
  11. * @copyright Copyright 2014-2018 SpryMedia Ltd.
  12. *
  13. * This source file is free software, available under the following license:
  14. * MIT license - http://datatables.net/license/mit
  15. *
  16. * This source file is distributed in the hope that it will be useful, but
  17. * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
  18. * or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details.
  19. *
  20. * For details please refer to: http://www.datatables.net
  21. */
  22. (function( factory ){
  23. if ( typeof define === 'function' && define.amd ) {
  24. // AMD
  25. define( ['jquery', 'datatables.net'], function ( $ ) {
  26. return factory( $, window, document );
  27. } );
  28. }
  29. else if ( typeof exports === 'object' ) {
  30. // CommonJS
  31. module.exports = function (root, $) {
  32. if ( ! root ) {
  33. root = window;
  34. }
  35. if ( ! $ || ! $.fn.dataTable ) {
  36. $ = require('datatables.net')(root, $).$;
  37. }
  38. return factory( $, root, root.document );
  39. };
  40. }
  41. else {
  42. // Browser
  43. factory( jQuery, window, document );
  44. }
  45. }(function( $, window, document, undefined ) {
  46. 'use strict';
  47. var DataTable = $.fn.dataTable;
  48. /**
  49. * Responsive is a plug-in for the DataTables library that makes use of
  50. * DataTables' ability to change the visibility of columns, changing the
  51. * visibility of columns so the displayed columns fit into the table container.
  52. * The end result is that complex tables will be dynamically adjusted to fit
  53. * into the viewport, be it on a desktop, tablet or mobile browser.
  54. *
  55. * Responsive for DataTables has two modes of operation, which can used
  56. * individually or combined:
  57. *
  58. * * Class name based control - columns assigned class names that match the
  59. * breakpoint logic can be shown / hidden as required for each breakpoint.
  60. * * Automatic control - columns are automatically hidden when there is no
  61. * room left to display them. Columns removed from the right.
  62. *
  63. * In additional to column visibility control, Responsive also has built into
  64. * options to use DataTables' child row display to show / hide the information
  65. * from the table that has been hidden. There are also two modes of operation
  66. * for this child row display:
  67. *
  68. * * Inline - when the control element that the user can use to show / hide
  69. * child rows is displayed inside the first column of the table.
  70. * * Column - where a whole column is dedicated to be the show / hide control.
  71. *
  72. * Initialisation of Responsive is performed by:
  73. *
  74. * * Adding the class `responsive` or `dt-responsive` to the table. In this case
  75. * Responsive will automatically be initialised with the default configuration
  76. * options when the DataTable is created.
  77. * * Using the `responsive` option in the DataTables configuration options. This
  78. * can also be used to specify the configuration options, or simply set to
  79. * `true` to use the defaults.
  80. *
  81. * @class
  82. * @param {object} settings DataTables settings object for the host table
  83. * @param {object} [opts] Configuration options
  84. * @requires jQuery 1.7+
  85. * @requires DataTables 1.10.3+
  86. *
  87. * @example
  88. * $('#example').DataTable( {
  89. * responsive: true
  90. * } );
  91. * } );
  92. */
  93. var Responsive = function ( settings, opts ) {
  94. // Sanity check that we are using DataTables 1.10 or newer
  95. if ( ! DataTable.versionCheck || ! DataTable.versionCheck( '1.10.10' ) ) {
  96. throw 'DataTables Responsive requires DataTables 1.10.10 or newer';
  97. }
  98. this.s = {
  99. dt: new DataTable.Api( settings ),
  100. columns: [],
  101. current: []
  102. };
  103. // Check if responsive has already been initialised on this table
  104. if ( this.s.dt.settings()[0].responsive ) {
  105. return;
  106. }
  107. // details is an object, but for simplicity the user can give it as a string
  108. // or a boolean
  109. if ( opts && typeof opts.details === 'string' ) {
  110. opts.details = { type: opts.details };
  111. }
  112. else if ( opts && opts.details === false ) {
  113. opts.details = { type: false };
  114. }
  115. else if ( opts && opts.details === true ) {
  116. opts.details = { type: 'inline' };
  117. }
  118. this.c = $.extend( true, {}, Responsive.defaults, DataTable.defaults.responsive, opts );
  119. settings.responsive = this;
  120. this._constructor();
  121. };
  122. $.extend( Responsive.prototype, {
  123. /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
  124. * Constructor
  125. */
  126. /**
  127. * Initialise the Responsive instance
  128. *
  129. * @private
  130. */
  131. _constructor: function ()
  132. {
  133. var that = this;
  134. var dt = this.s.dt;
  135. var dtPrivateSettings = dt.settings()[0];
  136. var oldWindowWidth = $(window).width();
  137. dt.settings()[0]._responsive = this;
  138. // Use DataTables' throttle function to avoid processor thrashing on
  139. // resize
  140. $(window).on( 'resize.dtr orientationchange.dtr', DataTable.util.throttle( function () {
  141. // iOS has a bug whereby resize can fire when only scrolling
  142. // See: http://stackoverflow.com/questions/8898412
  143. var width = $(window).width();
  144. if ( width !== oldWindowWidth ) {
  145. that._resize();
  146. oldWindowWidth = width;
  147. }
  148. } ) );
  149. // DataTables doesn't currently trigger an event when a row is added, so
  150. // we need to hook into its private API to enforce the hidden rows when
  151. // new data is added
  152. dtPrivateSettings.oApi._fnCallbackReg( dtPrivateSettings, 'aoRowCreatedCallback', function (tr, data, idx) {
  153. if ( $.inArray( false, that.s.current ) !== -1 ) {
  154. $('>td, >th', tr).each( function ( i ) {
  155. var idx = dt.column.index( 'toData', i );
  156. if ( that.s.current[idx] === false ) {
  157. $(this).css('display', 'none');
  158. }
  159. } );
  160. }
  161. } );
  162. // Destroy event handler
  163. dt.on( 'destroy.dtr', function () {
  164. dt.off( '.dtr' );
  165. $( dt.table().body() ).off( '.dtr' );
  166. $(window).off( 'resize.dtr orientationchange.dtr' );
  167. // Restore the columns that we've hidden
  168. $.each( that.s.current, function ( i, val ) {
  169. if ( val === false ) {
  170. that._setColumnVis( i, true );
  171. }
  172. } );
  173. } );
  174. // Reorder the breakpoints array here in case they have been added out
  175. // of order
  176. this.c.breakpoints.sort( function (a, b) {
  177. return a.width < b.width ? 1 :
  178. a.width > b.width ? -1 : 0;
  179. } );
  180. this._classLogic();
  181. this._resizeAuto();
  182. // Details handler
  183. var details = this.c.details;
  184. if ( details.type !== false ) {
  185. that._detailsInit();
  186. // DataTables will trigger this event on every column it shows and
  187. // hides individually
  188. dt.on( 'column-visibility.dtr', function () {
  189. // Use a small debounce to allow multiple columns to be set together
  190. if ( that._timer ) {
  191. clearTimeout( that._timer );
  192. }
  193. that._timer = setTimeout( function () {
  194. that._timer = null;
  195. that._classLogic();
  196. that._resizeAuto();
  197. that._resize();
  198. that._redrawChildren();
  199. }, 100 );
  200. } );
  201. // Redraw the details box on each draw which will happen if the data
  202. // has changed. This is used until DataTables implements a native
  203. // `updated` event for rows
  204. dt.on( 'draw.dtr', function () {
  205. that._redrawChildren();
  206. } );
  207. $(dt.table().node()).addClass( 'dtr-'+details.type );
  208. }
  209. dt.on( 'column-reorder.dtr', function (e, settings, details) {
  210. that._classLogic();
  211. that._resizeAuto();
  212. that._resize();
  213. } );
  214. // Change in column sizes means we need to calc
  215. dt.on( 'column-sizing.dtr', function () {
  216. that._resizeAuto();
  217. that._resize();
  218. });
  219. // On Ajax reload we want to reopen any child rows which are displayed
  220. // by responsive
  221. dt.on( 'preXhr.dtr', function () {
  222. var rowIds = [];
  223. dt.rows().every( function () {
  224. if ( this.child.isShown() ) {
  225. rowIds.push( this.id(true) );
  226. }
  227. } );
  228. dt.one( 'draw.dtr', function () {
  229. that._resizeAuto();
  230. that._resize();
  231. dt.rows( rowIds ).every( function () {
  232. that._detailsDisplay( this, false );
  233. } );
  234. } );
  235. });
  236. dt.on( 'init.dtr', function (e, settings, details) {
  237. that._resizeAuto();
  238. that._resize();
  239. // If columns were hidden, then DataTables needs to adjust the
  240. // column sizing
  241. if ( $.inArray( false, that.s.current ) ) {
  242. dt.columns.adjust();
  243. }
  244. } );
  245. // First pass - draw the table for the current viewport size
  246. this._resize();
  247. },
  248. /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
  249. * Private methods
  250. */
  251. /**
  252. * Calculate the visibility for the columns in a table for a given
  253. * breakpoint. The result is pre-determined based on the class logic if
  254. * class names are used to control all columns, but the width of the table
  255. * is also used if there are columns which are to be automatically shown
  256. * and hidden.
  257. *
  258. * @param {string} breakpoint Breakpoint name to use for the calculation
  259. * @return {array} Array of boolean values initiating the visibility of each
  260. * column.
  261. * @private
  262. */
  263. _columnsVisiblity: function ( breakpoint )
  264. {
  265. var dt = this.s.dt;
  266. var columns = this.s.columns;
  267. var i, ien;
  268. // Create an array that defines the column ordering based first on the
  269. // column's priority, and secondly the column index. This allows the
  270. // columns to be removed from the right if the priority matches
  271. var order = columns
  272. .map( function ( col, idx ) {
  273. return {
  274. columnIdx: idx,
  275. priority: col.priority
  276. };
  277. } )
  278. .sort( function ( a, b ) {
  279. if ( a.priority !== b.priority ) {
  280. return a.priority - b.priority;
  281. }
  282. return a.columnIdx - b.columnIdx;
  283. } );
  284. // Class logic - determine which columns are in this breakpoint based
  285. // on the classes. If no class control (i.e. `auto`) then `-` is used
  286. // to indicate this to the rest of the function
  287. var display = $.map( columns, function ( col, i ) {
  288. if ( dt.column(i).visible() === false ) {
  289. return 'not-visible';
  290. }
  291. return col.auto && col.minWidth === null ?
  292. false :
  293. col.auto === true ?
  294. '-' :
  295. $.inArray( breakpoint, col.includeIn ) !== -1;
  296. } );
  297. // Auto column control - first pass: how much width is taken by the
  298. // ones that must be included from the non-auto columns
  299. var requiredWidth = 0;
  300. for ( i=0, ien=display.length ; i<ien ; i++ ) {
  301. if ( display[i] === true ) {
  302. requiredWidth += columns[i].minWidth;
  303. }
  304. }
  305. // Second pass, use up any remaining width for other columns. For
  306. // scrolling tables we need to subtract the width of the scrollbar. It
  307. // may not be requires which makes this sub-optimal, but it would
  308. // require another full redraw to make complete use of those extra few
  309. // pixels
  310. var scrolling = dt.settings()[0].oScroll;
  311. var bar = scrolling.sY || scrolling.sX ? scrolling.iBarWidth : 0;
  312. var widthAvailable = dt.table().container().offsetWidth - bar;
  313. var usedWidth = widthAvailable - requiredWidth;
  314. // Control column needs to always be included. This makes it sub-
  315. // optimal in terms of using the available with, but to stop layout
  316. // thrashing or overflow. Also we need to account for the control column
  317. // width first so we know how much width is available for the other
  318. // columns, since the control column might not be the first one shown
  319. for ( i=0, ien=display.length ; i<ien ; i++ ) {
  320. if ( columns[i].control ) {
  321. usedWidth -= columns[i].minWidth;
  322. }
  323. }
  324. // Allow columns to be shown (counting by priority and then right to
  325. // left) until we run out of room
  326. var empty = false;
  327. for ( i=0, ien=order.length ; i<ien ; i++ ) {
  328. var colIdx = order[i].columnIdx;
  329. if ( display[colIdx] === '-' && ! columns[colIdx].control && columns[colIdx].minWidth ) {
  330. // Once we've found a column that won't fit we don't let any
  331. // others display either, or columns might disappear in the
  332. // middle of the table
  333. if ( empty || usedWidth - columns[colIdx].minWidth < 0 ) {
  334. empty = true;
  335. display[colIdx] = false;
  336. }
  337. else {
  338. display[colIdx] = true;
  339. }
  340. usedWidth -= columns[colIdx].minWidth;
  341. }
  342. }
  343. // Determine if the 'control' column should be shown (if there is one).
  344. // This is the case when there is a hidden column (that is not the
  345. // control column). The two loops look inefficient here, but they are
  346. // trivial and will fly through. We need to know the outcome from the
  347. // first , before the action in the second can be taken
  348. var showControl = false;
  349. for ( i=0, ien=columns.length ; i<ien ; i++ ) {
  350. if ( ! columns[i].control && ! columns[i].never && display[i] === false ) {
  351. showControl = true;
  352. break;
  353. }
  354. }
  355. for ( i=0, ien=columns.length ; i<ien ; i++ ) {
  356. if ( columns[i].control ) {
  357. display[i] = showControl;
  358. }
  359. // Replace not visible string with false from the control column detection above
  360. if ( display[i] === 'not-visible' ) {
  361. display[i] = false;
  362. }
  363. }
  364. // Finally we need to make sure that there is at least one column that
  365. // is visible
  366. if ( $.inArray( true, display ) === -1 ) {
  367. display[0] = true;
  368. }
  369. return display;
  370. },
  371. /**
  372. * Create the internal `columns` array with information about the columns
  373. * for the table. This includes determining which breakpoints the column
  374. * will appear in, based upon class names in the column, which makes up the
  375. * vast majority of this method.
  376. *
  377. * @private
  378. */
  379. _classLogic: function ()
  380. {
  381. var that = this;
  382. var calc = {};
  383. var breakpoints = this.c.breakpoints;
  384. var dt = this.s.dt;
  385. var columns = dt.columns().eq(0).map( function (i) {
  386. var column = this.column(i);
  387. var className = column.header().className;
  388. var priority = dt.settings()[0].aoColumns[i].responsivePriority;
  389. if ( priority === undefined ) {
  390. var dataPriority = $(column.header()).data('priority');
  391. priority = dataPriority !== undefined ?
  392. dataPriority * 1 :
  393. 10000;
  394. }
  395. return {
  396. className: className,
  397. includeIn: [],
  398. auto: false,
  399. control: false,
  400. never: className.match(/\bnever\b/) ? true : false,
  401. priority: priority
  402. };
  403. } );
  404. // Simply add a breakpoint to `includeIn` array, ensuring that there are
  405. // no duplicates
  406. var add = function ( colIdx, name ) {
  407. var includeIn = columns[ colIdx ].includeIn;
  408. if ( $.inArray( name, includeIn ) === -1 ) {
  409. includeIn.push( name );
  410. }
  411. };
  412. var column = function ( colIdx, name, operator, matched ) {
  413. var size, i, ien;
  414. if ( ! operator ) {
  415. columns[ colIdx ].includeIn.push( name );
  416. }
  417. else if ( operator === 'max-' ) {
  418. // Add this breakpoint and all smaller
  419. size = that._find( name ).width;
  420. for ( i=0, ien=breakpoints.length ; i<ien ; i++ ) {
  421. if ( breakpoints[i].width <= size ) {
  422. add( colIdx, breakpoints[i].name );
  423. }
  424. }
  425. }
  426. else if ( operator === 'min-' ) {
  427. // Add this breakpoint and all larger
  428. size = that._find( name ).width;
  429. for ( i=0, ien=breakpoints.length ; i<ien ; i++ ) {
  430. if ( breakpoints[i].width >= size ) {
  431. add( colIdx, breakpoints[i].name );
  432. }
  433. }
  434. }
  435. else if ( operator === 'not-' ) {
  436. // Add all but this breakpoint
  437. for ( i=0, ien=breakpoints.length ; i<ien ; i++ ) {
  438. if ( breakpoints[i].name.indexOf( matched ) === -1 ) {
  439. add( colIdx, breakpoints[i].name );
  440. }
  441. }
  442. }
  443. };
  444. // Loop over each column and determine if it has a responsive control
  445. // class
  446. columns.each( function ( col, i ) {
  447. var classNames = col.className.split(' ');
  448. var hasClass = false;
  449. // Split the class name up so multiple rules can be applied if needed
  450. for ( var k=0, ken=classNames.length ; k<ken ; k++ ) {
  451. var className = $.trim( classNames[k] );
  452. if ( className === 'all' ) {
  453. // Include in all
  454. hasClass = true;
  455. col.includeIn = $.map( breakpoints, function (a) {
  456. return a.name;
  457. } );
  458. return;
  459. }
  460. else if ( className === 'none' || col.never ) {
  461. // Include in none (default) and no auto
  462. hasClass = true;
  463. return;
  464. }
  465. else if ( className === 'control' ) {
  466. // Special column that is only visible, when one of the other
  467. // columns is hidden. This is used for the details control
  468. hasClass = true;
  469. col.control = true;
  470. return;
  471. }
  472. $.each( breakpoints, function ( j, breakpoint ) {
  473. // Does this column have a class that matches this breakpoint?
  474. var brokenPoint = breakpoint.name.split('-');
  475. var re = new RegExp( '(min\\-|max\\-|not\\-)?('+brokenPoint[0]+')(\\-[_a-zA-Z0-9])?' );
  476. var match = className.match( re );
  477. if ( match ) {
  478. hasClass = true;
  479. if ( match[2] === brokenPoint[0] && match[3] === '-'+brokenPoint[1] ) {
  480. // Class name matches breakpoint name fully
  481. column( i, breakpoint.name, match[1], match[2]+match[3] );
  482. }
  483. else if ( match[2] === brokenPoint[0] && ! match[3] ) {
  484. // Class name matched primary breakpoint name with no qualifier
  485. column( i, breakpoint.name, match[1], match[2] );
  486. }
  487. }
  488. } );
  489. }
  490. // If there was no control class, then automatic sizing is used
  491. if ( ! hasClass ) {
  492. col.auto = true;
  493. }
  494. } );
  495. this.s.columns = columns;
  496. },
  497. /**
  498. * Show the details for the child row
  499. *
  500. * @param {DataTables.Api} row API instance for the row
  501. * @param {boolean} update Update flag
  502. * @private
  503. */
  504. _detailsDisplay: function ( row, update )
  505. {
  506. var that = this;
  507. var dt = this.s.dt;
  508. var details = this.c.details;
  509. if ( details && details.type !== false ) {
  510. var res = details.display( row, update, function () {
  511. return details.renderer(
  512. dt, row[0], that._detailsObj(row[0])
  513. );
  514. } );
  515. if ( res === true || res === false ) {
  516. $(dt.table().node()).triggerHandler( 'responsive-display.dt', [dt, row, res, update] );
  517. }
  518. }
  519. },
  520. /**
  521. * Initialisation for the details handler
  522. *
  523. * @private
  524. */
  525. _detailsInit: function ()
  526. {
  527. var that = this;
  528. var dt = this.s.dt;
  529. var details = this.c.details;
  530. // The inline type always uses the first child as the target
  531. if ( details.type === 'inline' ) {
  532. details.target = 'td:first-child, th:first-child';
  533. }
  534. // Keyboard accessibility
  535. dt.on( 'draw.dtr', function () {
  536. that._tabIndexes();
  537. } );
  538. that._tabIndexes(); // Initial draw has already happened
  539. $( dt.table().body() ).on( 'keyup.dtr', 'td, th', function (e) {
  540. if ( e.keyCode === 13 && $(this).data('dtr-keyboard') ) {
  541. $(this).click();
  542. }
  543. } );
  544. // type.target can be a string jQuery selector or a column index
  545. var target = details.target;
  546. var selector = typeof target === 'string' ? target : 'td, th';
  547. // Click handler to show / hide the details rows when they are available
  548. $( dt.table().body() )
  549. .on( 'click.dtr mousedown.dtr mouseup.dtr', selector, function (e) {
  550. // If the table is not collapsed (i.e. there is no hidden columns)
  551. // then take no action
  552. if ( ! $(dt.table().node()).hasClass('collapsed' ) ) {
  553. return;
  554. }
  555. // Check that the row is actually a DataTable's controlled node
  556. if ( $.inArray( $(this).closest('tr').get(0), dt.rows().nodes().toArray() ) === -1 ) {
  557. return;
  558. }
  559. // For column index, we determine if we should act or not in the
  560. // handler - otherwise it is already okay
  561. if ( typeof target === 'number' ) {
  562. var targetIdx = target < 0 ?
  563. dt.columns().eq(0).length + target :
  564. target;
  565. if ( dt.cell( this ).index().column !== targetIdx ) {
  566. return;
  567. }
  568. }
  569. // $().closest() includes itself in its check
  570. var row = dt.row( $(this).closest('tr') );
  571. // Check event type to do an action
  572. if ( e.type === 'click' ) {
  573. // The renderer is given as a function so the caller can execute it
  574. // only when they need (i.e. if hiding there is no point is running
  575. // the renderer)
  576. that._detailsDisplay( row, false );
  577. }
  578. else if ( e.type === 'mousedown' ) {
  579. // For mouse users, prevent the focus ring from showing
  580. $(this).css('outline', 'none');
  581. }
  582. else if ( e.type === 'mouseup' ) {
  583. // And then re-allow at the end of the click
  584. $(this).blur().css('outline', '');
  585. }
  586. } );
  587. },
  588. /**
  589. * Get the details to pass to a renderer for a row
  590. * @param {int} rowIdx Row index
  591. * @private
  592. */
  593. _detailsObj: function ( rowIdx )
  594. {
  595. var that = this;
  596. var dt = this.s.dt;
  597. return $.map( this.s.columns, function( col, i ) {
  598. // Never and control columns should not be passed to the renderer
  599. if ( col.never || col.control ) {
  600. return;
  601. }
  602. return {
  603. title: dt.settings()[0].aoColumns[ i ].sTitle,
  604. data: dt.cell( rowIdx, i ).render( that.c.orthogonal ),
  605. hidden: dt.column( i ).visible() && !that.s.current[ i ],
  606. columnIndex: i,
  607. rowIndex: rowIdx
  608. };
  609. } );
  610. },
  611. /**
  612. * Find a breakpoint object from a name
  613. *
  614. * @param {string} name Breakpoint name to find
  615. * @return {object} Breakpoint description object
  616. * @private
  617. */
  618. _find: function ( name )
  619. {
  620. var breakpoints = this.c.breakpoints;
  621. for ( var i=0, ien=breakpoints.length ; i<ien ; i++ ) {
  622. if ( breakpoints[i].name === name ) {
  623. return breakpoints[i];
  624. }
  625. }
  626. },
  627. /**
  628. * Re-create the contents of the child rows as the display has changed in
  629. * some way.
  630. *
  631. * @private
  632. */
  633. _redrawChildren: function ()
  634. {
  635. var that = this;
  636. var dt = this.s.dt;
  637. dt.rows( {page: 'current'} ).iterator( 'row', function ( settings, idx ) {
  638. var row = dt.row( idx );
  639. that._detailsDisplay( dt.row( idx ), true );
  640. } );
  641. },
  642. /**
  643. * Alter the table display for a resized viewport. This involves first
  644. * determining what breakpoint the window currently is in, getting the
  645. * column visibilities to apply and then setting them.
  646. *
  647. * @private
  648. */
  649. _resize: function ()
  650. {
  651. var that = this;
  652. var dt = this.s.dt;
  653. var width = $(window).width();
  654. var breakpoints = this.c.breakpoints;
  655. var breakpoint = breakpoints[0].name;
  656. var columns = this.s.columns;
  657. var i, ien;
  658. var oldVis = this.s.current.slice();
  659. // Determine what breakpoint we are currently at
  660. for ( i=breakpoints.length-1 ; i>=0 ; i-- ) {
  661. if ( width <= breakpoints[i].width ) {
  662. breakpoint = breakpoints[i].name;
  663. break;
  664. }
  665. }
  666. // Show the columns for that break point
  667. var columnsVis = this._columnsVisiblity( breakpoint );
  668. this.s.current = columnsVis;
  669. // Set the class before the column visibility is changed so event
  670. // listeners know what the state is. Need to determine if there are
  671. // any columns that are not visible but can be shown
  672. var collapsedClass = false;
  673. for ( i=0, ien=columns.length ; i<ien ; i++ ) {
  674. if ( columnsVis[i] === false && ! columns[i].never && ! columns[i].control && ! dt.column(i).visible() === false ) {
  675. collapsedClass = true;
  676. break;
  677. }
  678. }
  679. $( dt.table().node() ).toggleClass( 'collapsed', collapsedClass );
  680. var changed = false;
  681. var visible = 0;
  682. dt.columns().eq(0).each( function ( colIdx, i ) {
  683. if ( columnsVis[i] === true ) {
  684. visible++;
  685. }
  686. if ( columnsVis[i] !== oldVis[i] ) {
  687. changed = true;
  688. that._setColumnVis( colIdx, columnsVis[i] );
  689. }
  690. } );
  691. if ( changed ) {
  692. this._redrawChildren();
  693. // Inform listeners of the change
  694. $(dt.table().node()).trigger( 'responsive-resize.dt', [dt, this.s.current] );
  695. // If no records, update the "No records" display element
  696. if ( dt.page.info().recordsDisplay === 0 ) {
  697. $('td', dt.table().body()).eq(0).attr('colspan', visible);
  698. }
  699. }
  700. },
  701. /**
  702. * Determine the width of each column in the table so the auto column hiding
  703. * has that information to work with. This method is never going to be 100%
  704. * perfect since column widths can change slightly per page, but without
  705. * seriously compromising performance this is quite effective.
  706. *
  707. * @private
  708. */
  709. _resizeAuto: function ()
  710. {
  711. var dt = this.s.dt;
  712. var columns = this.s.columns;
  713. // Are we allowed to do auto sizing?
  714. if ( ! this.c.auto ) {
  715. return;
  716. }
  717. // Are there any columns that actually need auto-sizing, or do they all
  718. // have classes defined
  719. if ( $.inArray( true, $.map( columns, function (c) { return c.auto; } ) ) === -1 ) {
  720. return;
  721. }
  722. // Need to restore all children. They will be reinstated by a re-render
  723. if ( ! $.isEmptyObject( _childNodeStore ) ) {
  724. $.each( _childNodeStore, function ( key ) {
  725. var idx = key.split('-');
  726. _childNodesRestore( dt, idx[0]*1, idx[1]*1 );
  727. } );
  728. }
  729. // Clone the table with the current data in it
  730. var tableWidth = dt.table().node().offsetWidth;
  731. var columnWidths = dt.columns;
  732. var clonedTable = dt.table().node().cloneNode( false );
  733. var clonedHeader = $( dt.table().header().cloneNode( false ) ).appendTo( clonedTable );
  734. var clonedBody = $( dt.table().body() ).clone( false, false ).empty().appendTo( clonedTable ); // use jQuery because of IE8
  735. // Header
  736. var headerCells = dt.columns()
  737. .header()
  738. .filter( function (idx) {
  739. return dt.column(idx).visible();
  740. } )
  741. .to$()
  742. .clone( false )
  743. .css( 'display', 'table-cell' )
  744. .css( 'min-width', 0 );
  745. // Body rows - we don't need to take account of DataTables' column
  746. // visibility since we implement our own here (hence the `display` set)
  747. $(clonedBody)
  748. .append( $(dt.rows( { page: 'current' } ).nodes()).clone( false ) )
  749. .find( 'th, td' ).css( 'display', '' );
  750. // Footer
  751. var footer = dt.table().footer();
  752. if ( footer ) {
  753. var clonedFooter = $( footer.cloneNode( false ) ).appendTo( clonedTable );
  754. var footerCells = dt.columns()
  755. .footer()
  756. .filter( function (idx) {
  757. return dt.column(idx).visible();
  758. } )
  759. .to$()
  760. .clone( false )
  761. .css( 'display', 'table-cell' );
  762. $('<tr/>')
  763. .append( footerCells )
  764. .appendTo( clonedFooter );
  765. }
  766. $('<tr/>')
  767. .append( headerCells )
  768. .appendTo( clonedHeader );
  769. // In the inline case extra padding is applied to the first column to
  770. // give space for the show / hide icon. We need to use this in the
  771. // calculation
  772. if ( this.c.details.type === 'inline' ) {
  773. $(clonedTable).addClass( 'dtr-inline collapsed' );
  774. }
  775. // It is unsafe to insert elements with the same name into the DOM
  776. // multiple times. For example, cloning and inserting a checked radio
  777. // clears the chcecked state of the original radio.
  778. $( clonedTable ).find( '[name]' ).removeAttr( 'name' );
  779. // A position absolute table would take the table out of the flow of
  780. // our container element, bypassing the height and width (Scroller)
  781. $( clonedTable ).css( 'position', 'relative' )
  782. var inserted = $('<div/>')
  783. .css( {
  784. width: 1,
  785. height: 1,
  786. overflow: 'hidden',
  787. clear: 'both'
  788. } )
  789. .append( clonedTable );
  790. inserted.insertBefore( dt.table().node() );
  791. // The cloned header now contains the smallest that each column can be
  792. headerCells.each( function (i) {
  793. var idx = dt.column.index( 'fromVisible', i );
  794. columns[ idx ].minWidth = this.offsetWidth || 0;
  795. } );
  796. inserted.remove();
  797. },
  798. /**
  799. * Set a column's visibility.
  800. *
  801. * We don't use DataTables' column visibility controls in order to ensure
  802. * that column visibility can Responsive can no-exist. Since only IE8+ is
  803. * supported (and all evergreen browsers of course) the control of the
  804. * display attribute works well.
  805. *
  806. * @param {integer} col Column index
  807. * @param {boolean} showHide Show or hide (true or false)
  808. * @private
  809. */
  810. _setColumnVis: function ( col, showHide )
  811. {
  812. var dt = this.s.dt;
  813. var display = showHide ? '' : 'none'; // empty string will remove the attr
  814. $( dt.column( col ).header() ).css( 'display', display );
  815. $( dt.column( col ).footer() ).css( 'display', display );
  816. dt.column( col ).nodes().to$().css( 'display', display );
  817. // If the are child nodes stored, we might need to reinsert them
  818. if ( ! $.isEmptyObject( _childNodeStore ) ) {
  819. dt.cells( null, col ).indexes().each( function (idx) {
  820. _childNodesRestore( dt, idx.row, idx.column );
  821. } );
  822. }
  823. },
  824. /**
  825. * Update the cell tab indexes for keyboard accessibility. This is called on
  826. * every table draw - that is potentially inefficient, but also the least
  827. * complex option given that column visibility can change on the fly. Its a
  828. * shame user-focus was removed from CSS 3 UI, as it would have solved this
  829. * issue with a single CSS statement.
  830. *
  831. * @private
  832. */
  833. _tabIndexes: function ()
  834. {
  835. var dt = this.s.dt;
  836. var cells = dt.cells( { page: 'current' } ).nodes().to$();
  837. var ctx = dt.settings()[0];
  838. var target = this.c.details.target;
  839. cells.filter( '[data-dtr-keyboard]' ).removeData( '[data-dtr-keyboard]' );
  840. if ( typeof target === 'number' ) {
  841. dt.cells( null, target, { page: 'current' } ).nodes().to$()
  842. .attr( 'tabIndex', ctx.iTabIndex )
  843. .data( 'dtr-keyboard', 1 );
  844. }
  845. else {
  846. // This is a bit of a hack - we need to limit the selected nodes to just
  847. // those of this table
  848. if ( target === 'td:first-child, th:first-child' ) {
  849. target = '>td:first-child, >th:first-child';
  850. }
  851. $( target, dt.rows( { page: 'current' } ).nodes() )
  852. .attr( 'tabIndex', ctx.iTabIndex )
  853. .data( 'dtr-keyboard', 1 );
  854. }
  855. }
  856. } );
  857. /**
  858. * List of default breakpoints. Each item in the array is an object with two
  859. * properties:
  860. *
  861. * * `name` - the breakpoint name.
  862. * * `width` - the breakpoint width
  863. *
  864. * @name Responsive.breakpoints
  865. * @static
  866. */
  867. Responsive.breakpoints = [
  868. { name: 'desktop', width: Infinity },
  869. { name: 'tablet-l', width: 1024 },
  870. { name: 'tablet-p', width: 768 },
  871. { name: 'mobile-l', width: 480 },
  872. { name: 'mobile-p', width: 320 }
  873. ];
  874. /**
  875. * Display methods - functions which define how the hidden data should be shown
  876. * in the table.
  877. *
  878. * @namespace
  879. * @name Responsive.defaults
  880. * @static
  881. */
  882. Responsive.display = {
  883. childRow: function ( row, update, render ) {
  884. if ( update ) {
  885. if ( $(row.node()).hasClass('parent') ) {
  886. row.child( render(), 'child' ).show();
  887. return true;
  888. }
  889. }
  890. else {
  891. if ( ! row.child.isShown() ) {
  892. row.child( render(), 'child' ).show();
  893. $( row.node() ).addClass( 'parent' );
  894. return true;
  895. }
  896. else {
  897. row.child( false );
  898. $( row.node() ).removeClass( 'parent' );
  899. return false;
  900. }
  901. }
  902. },
  903. childRowImmediate: function ( row, update, render ) {
  904. if ( (! update && row.child.isShown()) || ! row.responsive.hasHidden() ) {
  905. // User interaction and the row is show, or nothing to show
  906. row.child( false );
  907. $( row.node() ).removeClass( 'parent' );
  908. return false;
  909. }
  910. else {
  911. // Display
  912. row.child( render(), 'child' ).show();
  913. $( row.node() ).addClass( 'parent' );
  914. return true;
  915. }
  916. },
  917. // This is a wrapper so the modal options for Bootstrap and jQuery UI can
  918. // have options passed into them. This specific one doesn't need to be a
  919. // function but it is for consistency in the `modal` name
  920. modal: function ( options ) {
  921. return function ( row, update, render ) {
  922. if ( ! update ) {
  923. // Show a modal
  924. var close = function () {
  925. modal.remove(); // will tidy events for us
  926. $(document).off( 'keypress.dtr' );
  927. };
  928. var modal = $('<div class="dtr-modal"/>')
  929. .append( $('<div class="dtr-modal-display"/>')
  930. .append( $('<div class="dtr-modal-content"/>')
  931. .append( render() )
  932. )
  933. .append( $('<div class="dtr-modal-close">&times;</div>' )
  934. .click( function () {
  935. close();
  936. } )
  937. )
  938. )
  939. .append( $('<div class="dtr-modal-background"/>')
  940. .click( function () {
  941. close();
  942. } )
  943. )
  944. .appendTo( 'body' );
  945. $(document).on( 'keyup.dtr', function (e) {
  946. if ( e.keyCode === 27 ) {
  947. e.stopPropagation();
  948. close();
  949. }
  950. } );
  951. }
  952. else {
  953. $('div.dtr-modal-content')
  954. .empty()
  955. .append( render() );
  956. }
  957. if ( options && options.header ) {
  958. $('div.dtr-modal-content').prepend(
  959. '<h2>'+options.header( row )+'</h2>'
  960. );
  961. }
  962. };
  963. }
  964. };
  965. var _childNodeStore = {};
  966. function _childNodes( dt, row, col ) {
  967. var name = row+'-'+col;
  968. if ( _childNodeStore[ name ] ) {
  969. return _childNodeStore[ name ];
  970. }
  971. // https://jsperf.com/childnodes-array-slice-vs-loop
  972. var nodes = [];
  973. var children = dt.cell( row, col ).node().childNodes;
  974. for ( var i=0, ien=children.length ; i<ien ; i++ ) {
  975. nodes.push( children[i] );
  976. }
  977. _childNodeStore[ name ] = nodes;
  978. return nodes;
  979. }
  980. function _childNodesRestore( dt, row, col ) {
  981. var name = row+'-'+col;
  982. if ( ! _childNodeStore[ name ] ) {
  983. return;
  984. }
  985. var node = dt.cell( row, col ).node();
  986. var store = _childNodeStore[ name ];
  987. var parent = store[0].parentNode;
  988. var parentChildren = parent.childNodes;
  989. var a = [];
  990. for ( var i=0, ien=parentChildren.length ; i<ien ; i++ ) {
  991. a.push( parentChildren[i] );
  992. }
  993. for ( var j=0, jen=a.length ; j<jen ; j++ ) {
  994. node.appendChild( a[j] );
  995. }
  996. _childNodeStore[ name ] = undefined;
  997. }
  998. /**
  999. * Display methods - functions which define how the hidden data should be shown
  1000. * in the table.
  1001. *
  1002. * @namespace
  1003. * @name Responsive.defaults
  1004. * @static
  1005. */
  1006. Responsive.renderer = {
  1007. listHiddenNodes: function () {
  1008. return function ( api, rowIdx, columns ) {
  1009. var ul = $('<ul data-dtr-index="'+rowIdx+'" class="dtr-details"/>');
  1010. var found = false;
  1011. var data = $.each( columns, function ( i, col ) {
  1012. if ( col.hidden ) {
  1013. $(
  1014. '<li data-dtr-index="'+col.columnIndex+'" data-dt-row="'+col.rowIndex+'" data-dt-column="'+col.columnIndex+'">'+
  1015. '<span class="dtr-title">'+
  1016. col.title+
  1017. '</span> '+
  1018. '</li>'
  1019. )
  1020. .append( $('<span class="dtr-data"/>').append( _childNodes( api, col.rowIndex, col.columnIndex ) ) )// api.cell( col.rowIndex, col.columnIndex ).node().childNodes ) )
  1021. .appendTo( ul );
  1022. found = true;
  1023. }
  1024. } );
  1025. return found ?
  1026. ul :
  1027. false;
  1028. };
  1029. },
  1030. listHidden: function () {
  1031. return function ( api, rowIdx, columns ) {
  1032. var data = $.map( columns, function ( col ) {
  1033. return col.hidden ?
  1034. '<li data-dtr-index="'+col.columnIndex+'" data-dt-row="'+col.rowIndex+'" data-dt-column="'+col.columnIndex+'">'+
  1035. '<span class="dtr-title">'+
  1036. col.title+
  1037. '</span> '+
  1038. '<span class="dtr-data">'+
  1039. col.data+
  1040. '</span>'+
  1041. '</li>' :
  1042. '';
  1043. } ).join('');
  1044. return data ?
  1045. $('<ul data-dtr-index="'+rowIdx+'" class="dtr-details"/>').append( data ) :
  1046. false;
  1047. }
  1048. },
  1049. tableAll: function ( options ) {
  1050. options = $.extend( {
  1051. tableClass: ''
  1052. }, options );
  1053. return function ( api, rowIdx, columns ) {
  1054. var data = $.map( columns, function ( col ) {
  1055. return '<tr data-dt-row="'+col.rowIndex+'" data-dt-column="'+col.columnIndex+'">'+
  1056. '<td>'+col.title+':'+'</td> '+
  1057. '<td>'+col.data+'</td>'+
  1058. '</tr>';
  1059. } ).join('');
  1060. return $('<table class="'+options.tableClass+' dtr-details" width="100%"/>').append( data );
  1061. }
  1062. }
  1063. };
  1064. /**
  1065. * Responsive default settings for initialisation
  1066. *
  1067. * @namespace
  1068. * @name Responsive.defaults
  1069. * @static
  1070. */
  1071. Responsive.defaults = {
  1072. /**
  1073. * List of breakpoints for the instance. Note that this means that each
  1074. * instance can have its own breakpoints. Additionally, the breakpoints
  1075. * cannot be changed once an instance has been creased.
  1076. *
  1077. * @type {Array}
  1078. * @default Takes the value of `Responsive.breakpoints`
  1079. */
  1080. breakpoints: Responsive.breakpoints,
  1081. /**
  1082. * Enable / disable auto hiding calculations. It can help to increase
  1083. * performance slightly if you disable this option, but all columns would
  1084. * need to have breakpoint classes assigned to them
  1085. *
  1086. * @type {Boolean}
  1087. * @default `true`
  1088. */
  1089. auto: true,
  1090. /**
  1091. * Details control. If given as a string value, the `type` property of the
  1092. * default object is set to that value, and the defaults used for the rest
  1093. * of the object - this is for ease of implementation.
  1094. *
  1095. * The object consists of the following properties:
  1096. *
  1097. * * `display` - A function that is used to show and hide the hidden details
  1098. * * `renderer` - function that is called for display of the child row data.
  1099. * The default function will show the data from the hidden columns
  1100. * * `target` - Used as the selector for what objects to attach the child
  1101. * open / close to
  1102. * * `type` - `false` to disable the details display, `inline` or `column`
  1103. * for the two control types
  1104. *
  1105. * @type {Object|string}
  1106. */
  1107. details: {
  1108. display: Responsive.display.childRow,
  1109. renderer: Responsive.renderer.listHidden(),
  1110. target: 0,
  1111. type: 'inline'
  1112. },
  1113. /**
  1114. * Orthogonal data request option. This is used to define the data type
  1115. * requested when Responsive gets the data to show in the child row.
  1116. *
  1117. * @type {String}
  1118. */
  1119. orthogonal: 'display'
  1120. };
  1121. /*
  1122. * API
  1123. */
  1124. var Api = $.fn.dataTable.Api;
  1125. // Doesn't do anything - work around for a bug in DT... Not documented
  1126. Api.register( 'responsive()', function () {
  1127. return this;
  1128. } );
  1129. Api.register( 'responsive.index()', function ( li ) {
  1130. li = $(li);
  1131. return {
  1132. column: li.data('dtr-index'),
  1133. row: li.parent().data('dtr-index')
  1134. };
  1135. } );
  1136. Api.register( 'responsive.rebuild()', function () {
  1137. return this.iterator( 'table', function ( ctx ) {
  1138. if ( ctx._responsive ) {
  1139. ctx._responsive._classLogic();
  1140. }
  1141. } );
  1142. } );
  1143. Api.register( 'responsive.recalc()', function () {
  1144. return this.iterator( 'table', function ( ctx ) {
  1145. if ( ctx._responsive ) {
  1146. ctx._responsive._resizeAuto();
  1147. ctx._responsive._resize();
  1148. }
  1149. } );
  1150. } );
  1151. Api.register( 'responsive.hasHidden()', function () {
  1152. var ctx = this.context[0];
  1153. return ctx._responsive ?
  1154. $.inArray( false, ctx._responsive.s.current ) !== -1 :
  1155. false;
  1156. } );
  1157. Api.registerPlural( 'columns().responsiveHidden()', 'column().responsiveHidden()', function () {
  1158. return this.iterator( 'column', function ( settings, column ) {
  1159. return settings._responsive ?
  1160. settings._responsive.s.current[ column ] :
  1161. false;
  1162. }, 1 );
  1163. } );
  1164. /**
  1165. * Version information
  1166. *
  1167. * @name Responsive.version
  1168. * @static
  1169. */
  1170. Responsive.version = '2.2.2';
  1171. $.fn.dataTable.Responsive = Responsive;
  1172. $.fn.DataTable.Responsive = Responsive;
  1173. // Attach a listener to the document which listens for DataTables initialisation
  1174. // events so we can automatically initialise
  1175. $(document).on( 'preInit.dt.dtr', function (e, settings, json) {
  1176. if ( e.namespace !== 'dt' ) {
  1177. return;
  1178. }
  1179. if ( $(settings.nTable).hasClass( 'responsive' ) ||
  1180. $(settings.nTable).hasClass( 'dt-responsive' ) ||
  1181. settings.oInit.responsive ||
  1182. DataTable.defaults.responsive
  1183. ) {
  1184. var init = settings.oInit.responsive;
  1185. if ( init !== false ) {
  1186. new Responsive( settings, $.isPlainObject( init ) ? init : {} );
  1187. }
  1188. }
  1189. } );
  1190. return Responsive;
  1191. }));