dataTables.responsive.js 37 KB

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