/*! Responsive 3.0.2 * © SpryMedia Ltd - datatables.net/license */ import jQuery from 'jquery'; import DataTable from 'datatables.net'; // Allow reassignment of the $ variable let $ = jQuery; /** * @summary Responsive * @description Responsive tables plug-in for DataTables * @version 3.0.2 * @author SpryMedia Ltd * @copyright SpryMedia Ltd. * * This source file is free software, available under the following license: * MIT license - http://datatables.net/license/mit * * This source file is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details. * * For details please refer to: http://www.datatables.net */ /** * Responsive is a plug-in for the DataTables library that makes use of * DataTables' ability to change the visibility of columns, changing the * visibility of columns so the displayed columns fit into the table container. * The end result is that complex tables will be dynamically adjusted to fit * into the viewport, be it on a desktop, tablet or mobile browser. * * Responsive for DataTables has two modes of operation, which can used * individually or combined: * * * Class name based control - columns assigned class names that match the * breakpoint logic can be shown / hidden as required for each breakpoint. * * Automatic control - columns are automatically hidden when there is no * room left to display them. Columns removed from the right. * * In additional to column visibility control, Responsive also has built into * options to use DataTables' child row display to show / hide the information * from the table that has been hidden. There are also two modes of operation * for this child row display: * * * Inline - when the control element that the user can use to show / hide * child rows is displayed inside the first column of the table. * * Column - where a whole column is dedicated to be the show / hide control. * * Initialisation of Responsive is performed by: * * * Adding the class `responsive` or `dt-responsive` to the table. In this case * Responsive will automatically be initialised with the default configuration * options when the DataTable is created. * * Using the `responsive` option in the DataTables configuration options. This * can also be used to specify the configuration options, or simply set to * `true` to use the defaults. * * @class * @param {object} settings DataTables settings object for the host table * @param {object} [opts] Configuration options * @requires jQuery 1.7+ * @requires DataTables 1.10.3+ * * @example * $('#example').DataTable( { * responsive: true * } ); * } ); */ var Responsive = function (settings, opts) { // Sanity check that we are using DataTables 1.10 or newer if (!DataTable.versionCheck || !DataTable.versionCheck('2')) { throw 'DataTables Responsive requires DataTables 2 or newer'; } this.s = { childNodeStore: {}, columns: [], current: [], dt: new DataTable.Api(settings) }; // Check if responsive has already been initialised on this table if (this.s.dt.settings()[0].responsive) { return; } // details is an object, but for simplicity the user can give it as a string // or a boolean if (opts && typeof opts.details === 'string') { opts.details = { type: opts.details }; } else if (opts && opts.details === false) { opts.details = { type: false }; } else if (opts && opts.details === true) { opts.details = { type: 'inline' }; } this.c = $.extend( true, {}, Responsive.defaults, DataTable.defaults.responsive, opts ); settings.responsive = this; this._constructor(); }; $.extend(Responsive.prototype, { /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Constructor */ /** * Initialise the Responsive instance * * @private */ _constructor: function () { var that = this; var dt = this.s.dt; var oldWindowWidth = $(window).innerWidth(); dt.settings()[0]._responsive = this; // Use DataTables' throttle function to avoid processor thrashing $(window).on( 'orientationchange.dtr', DataTable.util.throttle(function () { // iOS has a bug whereby resize can fire when only scrolling // See: http://stackoverflow.com/questions/8898412 var width = $(window).innerWidth(); if (width !== oldWindowWidth) { that._resize(); oldWindowWidth = width; } }) ); // Handle new rows being dynamically added - needed as responsive // updates all rows (shown or not) a responsive change, rather than // per draw. dt.on('row-created.dtr', function (e, tr, data, idx) { if ($.inArray(false, that.s.current) !== -1) { $('>td, >th', tr).each(function (i) { var idx = dt.column.index('toData', i); if (that.s.current[idx] === false) { $(this) .css('display', 'none') .addClass('dtr-hidden'); } }); } }); // Destroy event handler dt.on('destroy.dtr', function () { dt.off('.dtr'); $(dt.table().body()).off('.dtr'); $(window).off('resize.dtr orientationchange.dtr'); dt.cells('.dtr-control').nodes().to$().removeClass('dtr-control'); $(dt.table().node()).removeClass('dtr-inline collapsed'); // Restore the columns that we've hidden $.each(that.s.current, function (i, val) { if (val === false) { that._setColumnVis(i, true); } }); }); // Reorder the breakpoints array here in case they have been added out // of order this.c.breakpoints.sort(function (a, b) { return a.width < b.width ? 1 : a.width > b.width ? -1 : 0; }); this._classLogic(); this._resizeAuto(); // Details handler var details = this.c.details; if (details.type !== false) { that._detailsInit(); // DataTables will trigger this event on every column it shows and // hides individually dt.on('column-visibility.dtr', function () { // Use a small debounce to allow multiple columns to be set together if (that._timer) { clearTimeout(that._timer); } that._timer = setTimeout(function () { that._timer = null; that._classLogic(); that._resizeAuto(); that._resize(true); that._redrawChildren(); }, 100); }); // Redraw the details box on each draw which will happen if the data // has changed. This is used until DataTables implements a native // `updated` event for rows dt.on('draw.dtr', function () { that._redrawChildren(); }); $(dt.table().node()).addClass('dtr-' + details.type); } dt.on('column-reorder.dtr', function (e, settings, details) { that._classLogic(); that._resizeAuto(); that._resize(true); }); // Change in column sizes means we need to calc dt.on('column-sizing.dtr', function () { that._resizeAuto(); that._resize(); }); // DT2 let's us tell it if we are hiding columns dt.on('column-calc.dt', function (e, d) { var curr = that.s.current; for (var i = 0; i < curr.length; i++) { var idx = d.visible.indexOf(i); if (curr[i] === false && idx >= 0) { d.visible.splice(idx, 1); } } }); // On Ajax reload we want to reopen any child rows which are displayed // by responsive dt.on('preXhr.dtr', function () { var rowIds = []; dt.rows().every(function () { if (this.child.isShown()) { rowIds.push(this.id(true)); } }); dt.one('draw.dtr', function () { that._resizeAuto(); that._resize(); dt.rows(rowIds).every(function () { that._detailsDisplay(this, false); }); }); }); dt.on('draw.dtr', function () { that._controlClass(); }).on('init.dtr', function (e, settings, details) { if (e.namespace !== 'dt') { return; } that._resizeAuto(); that._resize(); }); // First pass - draw the table for the current viewport size this._resize(); }, /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Private methods */ /** * Insert a `col` tag into the correct location in a `colgroup`. * * @param {jQuery} colGroup The `colgroup` tag * @param {jQuery} colEl The `col` tag */ _colGroupAttach: function (colGroup, colEls, idx) { var found = null; // No need to do anything if already attached if (colEls[idx].get(0).parentNode === colGroup[0]) { return; } // Find the first `col` after our own which is already attached for (var i = idx+1; i < colEls.length; i++) { if (colGroup[0] === colEls[i].get(0).parentNode) { found = i; break; } } if (found !== null) { // Insert before colEls[idx].insertBefore(colEls[found][0]); } else { // If wasn't found, insert at the end colGroup.append(colEls[idx]); } }, /** * Get and store nodes from a cell - use for node moving renderers * * @param {*} dt DT instance * @param {*} row Row index * @param {*} col Column index */ _childNodes: function (dt, row, col) { var name = row + '-' + col; if (this.s.childNodeStore[name]) { return this.s.childNodeStore[name]; } // https://jsperf.com/childnodes-array-slice-vs-loop var nodes = []; var children = dt.cell(row, col).node().childNodes; for (var i = 0, ien = children.length; i < ien; i++) { nodes.push(children[i]); } this.s.childNodeStore[name] = nodes; return nodes; }, /** * Restore nodes from the cache to a table cell * * @param {*} dt DT instance * @param {*} row Row index * @param {*} col Column index */ _childNodesRestore: function (dt, row, col) { var name = row + '-' + col; if (!this.s.childNodeStore[name]) { return; } var node = dt.cell(row, col).node(); var store = this.s.childNodeStore[name]; if (store.length > 0) { var parent = store[0].parentNode; var parentChildren = parent.childNodes; var a = []; for (var i = 0, ien = parentChildren.length; i < ien; i++) { a.push(parentChildren[i]); } for (var j = 0, jen = a.length; j < jen; j++) { node.appendChild(a[j]); } } this.s.childNodeStore[name] = undefined; }, /** * Calculate the visibility for the columns in a table for a given * breakpoint. The result is pre-determined based on the class logic if * class names are used to control all columns, but the width of the table * is also used if there are columns which are to be automatically shown * and hidden. * * @param {string} breakpoint Breakpoint name to use for the calculation * @return {array} Array of boolean values initiating the visibility of each * column. * @private */ _columnsVisiblity: function (breakpoint) { var dt = this.s.dt; var columns = this.s.columns; var i, ien; // Create an array that defines the column ordering based first on the // column's priority, and secondly the column index. This allows the // columns to be removed from the right if the priority matches var order = columns .map(function (col, idx) { return { columnIdx: idx, priority: col.priority }; }) .sort(function (a, b) { if (a.priority !== b.priority) { return a.priority - b.priority; } return a.columnIdx - b.columnIdx; }); // Class logic - determine which columns are in this breakpoint based // on the classes. If no class control (i.e. `auto`) then `-` is used // to indicate this to the rest of the function var display = $.map(columns, function (col, i) { if (dt.column(i).visible() === false) { return 'not-visible'; } return col.auto && col.minWidth === null ? false : col.auto === true ? '-' : $.inArray(breakpoint, col.includeIn) !== -1; }); // Auto column control - first pass: how much width is taken by the // ones that must be included from the non-auto columns var requiredWidth = 0; for (i = 0, ien = display.length; i < ien; i++) { if (display[i] === true) { requiredWidth += columns[i].minWidth; } } // Second pass, use up any remaining width for other columns. For // scrolling tables we need to subtract the width of the scrollbar. It // may not be requires which makes this sub-optimal, but it would // require another full redraw to make complete use of those extra few // pixels var scrolling = dt.settings()[0].oScroll; var bar = scrolling.sY || scrolling.sX ? scrolling.iBarWidth : 0; var widthAvailable = dt.table().container().offsetWidth - bar; var usedWidth = widthAvailable - requiredWidth; // Control column needs to always be included. This makes it sub- // optimal in terms of using the available with, but to stop layout // thrashing or overflow. Also we need to account for the control column // width first so we know how much width is available for the other // columns, since the control column might not be the first one shown for (i = 0, ien = display.length; i < ien; i++) { if (columns[i].control) { usedWidth -= columns[i].minWidth; } } // Allow columns to be shown (counting by priority and then right to // left) until we run out of room var empty = false; for (i = 0, ien = order.length; i < ien; i++) { var colIdx = order[i].columnIdx; if ( display[colIdx] === '-' && !columns[colIdx].control && columns[colIdx].minWidth ) { // Once we've found a column that won't fit we don't let any // others display either, or columns might disappear in the // middle of the table if (empty || usedWidth - columns[colIdx].minWidth < 0) { empty = true; display[colIdx] = false; } else { display[colIdx] = true; } usedWidth -= columns[colIdx].minWidth; } } // Determine if the 'control' column should be shown (if there is one). // This is the case when there is a hidden column (that is not the // control column). The two loops look inefficient here, but they are // trivial and will fly through. We need to know the outcome from the // first , before the action in the second can be taken var showControl = false; for (i = 0, ien = columns.length; i < ien; i++) { if ( !columns[i].control && !columns[i].never && display[i] === false ) { showControl = true; break; } } for (i = 0, ien = columns.length; i < ien; i++) { if (columns[i].control) { display[i] = showControl; } // Replace not visible string with false from the control column detection above if (display[i] === 'not-visible') { display[i] = false; } } // Finally we need to make sure that there is at least one column that // is visible if ($.inArray(true, display) === -1) { display[0] = true; } return display; }, /** * Create the internal `columns` array with information about the columns * for the table. This includes determining which breakpoints the column * will appear in, based upon class names in the column, which makes up the * vast majority of this method. * * @private */ _classLogic: function () { var that = this; var breakpoints = this.c.breakpoints; var dt = this.s.dt; var columns = dt .columns() .eq(0) .map(function (i) { var column = this.column(i); var className = column.header().className; var priority = column.init().responsivePriority; var dataPriority = column .header() .getAttribute('data-priority'); if (priority === undefined) { priority = dataPriority === undefined || dataPriority === null ? 10000 : dataPriority * 1; } return { className: className, includeIn: [], auto: false, control: false, never: className.match(/\b(dtr\-)?never\b/) ? true : false, priority: priority }; }); // Simply add a breakpoint to `includeIn` array, ensuring that there are // no duplicates var add = function (colIdx, name) { var includeIn = columns[colIdx].includeIn; if ($.inArray(name, includeIn) === -1) { includeIn.push(name); } }; var column = function (colIdx, name, operator, matched) { var size, i, ien; if (!operator) { columns[colIdx].includeIn.push(name); } else if (operator === 'max-') { // Add this breakpoint and all smaller size = that._find(name).width; for (i = 0, ien = breakpoints.length; i < ien; i++) { if (breakpoints[i].width <= size) { add(colIdx, breakpoints[i].name); } } } else if (operator === 'min-') { // Add this breakpoint and all larger size = that._find(name).width; for (i = 0, ien = breakpoints.length; i < ien; i++) { if (breakpoints[i].width >= size) { add(colIdx, breakpoints[i].name); } } } else if (operator === 'not-') { // Add all but this breakpoint for (i = 0, ien = breakpoints.length; i < ien; i++) { if (breakpoints[i].name.indexOf(matched) === -1) { add(colIdx, breakpoints[i].name); } } } }; // Loop over each column and determine if it has a responsive control // class columns.each(function (col, i) { var classNames = col.className.split(' '); var hasClass = false; // Split the class name up so multiple rules can be applied if needed for (var k = 0, ken = classNames.length; k < ken; k++) { var className = classNames[k].trim(); if (className === 'all' || className === 'dtr-all') { // Include in all hasClass = true; col.includeIn = $.map(breakpoints, function (a) { return a.name; }); return; } else if ( className === 'none' || className === 'dtr-none' || col.never ) { // Include in none (default) and no auto hasClass = true; return; } else if ( className === 'control' || className === 'dtr-control' ) { // Special column that is only visible, when one of the other // columns is hidden. This is used for the details control hasClass = true; col.control = true; return; } $.each(breakpoints, function (j, breakpoint) { // Does this column have a class that matches this breakpoint? var brokenPoint = breakpoint.name.split('-'); var re = new RegExp( '(min\\-|max\\-|not\\-)?(' + brokenPoint[0] + ')(\\-[_a-zA-Z0-9])?' ); var match = className.match(re); if (match) { hasClass = true; if ( match[2] === brokenPoint[0] && match[3] === '-' + brokenPoint[1] ) { // Class name matches breakpoint name fully column( i, breakpoint.name, match[1], match[2] + match[3] ); } else if (match[2] === brokenPoint[0] && !match[3]) { // Class name matched primary breakpoint name with no qualifier column(i, breakpoint.name, match[1], match[2]); } } }); } // If there was no control class, then automatic sizing is used if (!hasClass) { col.auto = true; } }); this.s.columns = columns; }, /** * Update the cells to show the correct control class / button * @private */ _controlClass: function () { if (this.c.details.type === 'inline') { var dt = this.s.dt; var columnsVis = this.s.current; var firstVisible = $.inArray(true, columnsVis); // Remove from any cells which shouldn't have it dt.cells( null, function (idx) { return idx !== firstVisible; }, { page: 'current' } ) .nodes() .to$() .filter('.dtr-control') .removeClass('dtr-control'); dt.cells(null, firstVisible, { page: 'current' }) .nodes() .to$() .addClass('dtr-control'); } }, /** * Show the details for the child row * * @param {DataTables.Api} row API instance for the row * @param {boolean} update Update flag * @private */ _detailsDisplay: function (row, update) { var that = this; var dt = this.s.dt; var details = this.c.details; var event = function (res) { $(row.node()).toggleClass('dtr-expanded', res !== false); $(dt.table().node()).triggerHandler('responsive-display.dt', [ dt, row, res, update ]); }; if (details && details.type !== false) { var renderer = typeof details.renderer === 'string' ? Responsive.renderer[details.renderer]() : details.renderer; var res = details.display( row, update, function () { return renderer.call( that, dt, row[0][0], that._detailsObj(row[0]) ); }, function () { event(false); } ); if (typeof res === 'boolean') { event(res); } } }, /** * Initialisation for the details handler * * @private */ _detailsInit: function () { var that = this; var dt = this.s.dt; var details = this.c.details; // The inline type always uses the first child as the target if (details.type === 'inline') { details.target = 'td.dtr-control, th.dtr-control'; } // Keyboard accessibility dt.on('draw.dtr', function () { that._tabIndexes(); }); that._tabIndexes(); // Initial draw has already happened $(dt.table().body()).on('keyup.dtr', 'td, th', function (e) { if (e.keyCode === 13 && $(this).data('dtr-keyboard')) { $(this).click(); } }); // type.target can be a string jQuery selector or a column index var target = details.target; var selector = typeof target === 'string' ? target : 'td, th'; if (target !== undefined || target !== null) { // Click handler to show / hide the details rows when they are available $(dt.table().body()).on( 'click.dtr mousedown.dtr mouseup.dtr', selector, function (e) { // If the table is not collapsed (i.e. there is no hidden columns) // then take no action if (!$(dt.table().node()).hasClass('collapsed')) { return; } // Check that the row is actually a DataTable's controlled node if ( $.inArray( $(this).closest('tr').get(0), dt.rows().nodes().toArray() ) === -1 ) { return; } // For column index, we determine if we should act or not in the // handler - otherwise it is already okay if (typeof target === 'number') { var targetIdx = target < 0 ? dt.columns().eq(0).length + target : target; if (dt.cell(this).index().column !== targetIdx) { return; } } // $().closest() includes itself in its check var row = dt.row($(this).closest('tr')); // Check event type to do an action if (e.type === 'click') { // The renderer is given as a function so the caller can execute it // only when they need (i.e. if hiding there is no point is running // the renderer) that._detailsDisplay(row, false); } else if (e.type === 'mousedown') { // For mouse users, prevent the focus ring from showing $(this).css('outline', 'none'); } else if (e.type === 'mouseup') { // And then re-allow at the end of the click $(this).trigger('blur').css('outline', ''); } } ); } }, /** * Get the details to pass to a renderer for a row * @param {int} rowIdx Row index * @private */ _detailsObj: function (rowIdx) { var that = this; var dt = this.s.dt; return $.map(this.s.columns, function (col, i) { // Never and control columns should not be passed to the renderer if (col.never || col.control) { return; } var dtCol = dt.settings()[0].aoColumns[i]; return { className: dtCol.sClass, columnIndex: i, data: dt.cell(rowIdx, i).render(that.c.orthogonal), hidden: dt.column(i).visible() && !that.s.current[i], rowIndex: rowIdx, title: dt.column(i).title() }; }); }, /** * Find a breakpoint object from a name * * @param {string} name Breakpoint name to find * @return {object} Breakpoint description object * @private */ _find: function (name) { var breakpoints = this.c.breakpoints; for (var i = 0, ien = breakpoints.length; i < ien; i++) { if (breakpoints[i].name === name) { return breakpoints[i]; } } }, /** * Re-create the contents of the child rows as the display has changed in * some way. * * @private */ _redrawChildren: function () { var that = this; var dt = this.s.dt; dt.rows({ page: 'current' }).iterator('row', function (settings, idx) { that._detailsDisplay(dt.row(idx), true); }); }, /** * Alter the table display for a resized viewport. This involves first * determining what breakpoint the window currently is in, getting the * column visibilities to apply and then setting them. * * @param {boolean} forceRedraw Force a redraw * @private */ _resize: function (forceRedraw) { var that = this; var dt = this.s.dt; var width = $(window).innerWidth(); var breakpoints = this.c.breakpoints; var breakpoint = breakpoints[0].name; var columns = this.s.columns; var i, ien; var oldVis = this.s.current.slice(); // Determine what breakpoint we are currently at for (i = breakpoints.length - 1; i >= 0; i--) { if (width <= breakpoints[i].width) { breakpoint = breakpoints[i].name; break; } } // Show the columns for that break point var columnsVis = this._columnsVisiblity(breakpoint); this.s.current = columnsVis; // Set the class before the column visibility is changed so event // listeners know what the state is. Need to determine if there are // any columns that are not visible but can be shown var collapsedClass = false; for (i = 0, ien = columns.length; i < ien; i++) { if ( columnsVis[i] === false && !columns[i].never && !columns[i].control && !dt.column(i).visible() === false ) { collapsedClass = true; break; } } $(dt.table().node()).toggleClass('collapsed', collapsedClass); var changed = false; var visible = 0; var dtSettings = dt.settings()[0]; var colGroup = $(dt.table().node()).children('colgroup'); var colEls = dtSettings.aoColumns.map(function (col) { return col.colEl; }); dt.columns() .eq(0) .each(function (colIdx, i) { //console.log(colIdx, i); // Do nothing on DataTables' hidden column - DT removes it from the table // so we need to slide back if (! dt.column(colIdx).visible()) { return; } if (columnsVis[i] === true) { visible++; } if (forceRedraw || columnsVis[i] !== oldVis[i]) { changed = true; that._setColumnVis(colIdx, columnsVis[i]); } // DataTables 2 uses `col` to define the width for a column // and this needs to run each time, as DataTables will change // the column width. We may need to reattach if we've removed // an element previously. if (! columnsVis[i]) { colEls[i].detach(); } else { that._colGroupAttach(colGroup, colEls, i); } }); if (changed) { dt.columns.adjust(); this._redrawChildren(); // Inform listeners of the change $(dt.table().node()).trigger('responsive-resize.dt', [ dt, this._responsiveOnlyHidden() ]); // If no records, update the "No records" display element if (dt.page.info().recordsDisplay === 0) { $('td', dt.table().body()).eq(0).attr('colspan', visible); } } that._controlClass(); }, /** * Determine the width of each column in the table so the auto column hiding * has that information to work with. This method is never going to be 100% * perfect since column widths can change slightly per page, but without * seriously compromising performance this is quite effective. * * @private */ _resizeAuto: function () { var dt = this.s.dt; var columns = this.s.columns; var that = this; var visibleColumns = dt .columns() .indexes() .filter(function (idx) { return dt.column(idx).visible(); }); // Are we allowed to do auto sizing? if (!this.c.auto) { return; } // Are there any columns that actually need auto-sizing, or do they all // have classes defined if ( $.inArray( true, $.map(columns, function (c) { return c.auto; }) ) === -1 ) { return; } // Clone the table with the current data in it var clonedTable = dt.table().node().cloneNode(false); var clonedHeader = $(dt.table().header().cloneNode(false)).appendTo( clonedTable ); var clonedFooter = $(dt.table().footer().cloneNode(false)).appendTo( clonedTable ); var clonedBody = $(dt.table().body()) .clone(false, false) .empty() .appendTo(clonedTable); // use jQuery because of IE8 clonedTable.style.width = 'auto'; // Header dt.table() .header.structure(visibleColumns) .forEach((row) => { var cells = row .filter(function (el) { return el ? true : false; }) .map(function (el) { return $(el.cell) .clone(false) .css('display', 'table-cell') .css('width', 'auto') .css('min-width', 0); }); $('').append(cells).appendTo(clonedHeader); }); // Always need an empty row that we can read widths from var emptyRow = $('').appendTo(clonedBody); for (var i = 0; i < visibleColumns.count(); i++) { emptyRow.append(''); } // Body rows dt.rows({ page: 'current' }).every(function (rowIdx) { var node = this.node(); if (! node) { return; } // We clone the table's rows and cells to create the sizing table var tr = node.cloneNode(false); dt.cells(rowIdx, visibleColumns).every(function (rowIdx2, colIdx) { // If nodes have been moved out (listHiddenNodes), we need to // clone from the store var store = that.s.childNodeStore[rowIdx + '-' + colIdx]; if (store) { $(this.node().cloneNode(false)) .append($(store).clone()) .appendTo(tr); } else { $(this.node()).clone(false).appendTo(tr); } }); clonedBody.append(tr); }); // Any cells which were hidden by Responsive in the host table, need to // be visible here for the calculations clonedBody.find('th, td').css('display', ''); // Footer dt.table() .footer.structure(visibleColumns) .forEach((row) => { var cells = row .filter(function (el) { return el ? true : false; }) .map(function (el) { return $(el.cell) .clone(false) .css('display', 'table-cell') .css('width', 'auto') .css('min-width', 0); }); $('').append(cells).appendTo(clonedFooter); }); // In the inline case extra padding is applied to the first column to // give space for the show / hide icon. We need to use this in the // calculation if (this.c.details.type === 'inline') { $(clonedTable).addClass('dtr-inline collapsed'); } // It is unsafe to insert elements with the same name into the DOM // multiple times. For example, cloning and inserting a checked radio // clears the chcecked state of the original radio. $(clonedTable).find('[name]').removeAttr('name'); // A position absolute table would take the table out of the flow of // our container element, bypassing the height and width (Scroller) $(clonedTable).css('position', 'relative'); var inserted = $('
') .css({ width: 1, height: 1, overflow: 'hidden', clear: 'both' }) .append(clonedTable); inserted.insertBefore(dt.table().node()); // The cloned table now contains the smallest that each column can be emptyRow.children().each(function (i) { var idx = dt.column.index('fromVisible', i); columns[idx].minWidth = this.offsetWidth || 0; }); inserted.remove(); }, /** * Get the state of the current hidden columns - controlled by Responsive only */ _responsiveOnlyHidden: function () { var dt = this.s.dt; return $.map(this.s.current, function (v, i) { // If the column is hidden by DataTables then it can't be hidden by // Responsive! if (dt.column(i).visible() === false) { return true; } return v; }); }, /** * Set a column's visibility. * * We don't use DataTables' column visibility controls in order to ensure * that column visibility can Responsive can no-exist. Since only IE8+ is * supported (and all evergreen browsers of course) the control of the * display attribute works well. * * @param {integer} col Column index * @param {boolean} showHide Show or hide (true or false) * @private */ _setColumnVis: function (col, showHide) { var that = this; var dt = this.s.dt; var display = showHide ? '' : 'none'; // empty string will remove the attr this._setHeaderVis(col, showHide, dt.table().header.structure()); this._setHeaderVis(col, showHide, dt.table().footer.structure()); dt.column(col) .nodes() .to$() .css('display', display) .toggleClass('dtr-hidden', !showHide); // If the are child nodes stored, we might need to reinsert them if (!$.isEmptyObject(this.s.childNodeStore)) { dt.cells(null, col) .indexes() .each(function (idx) { that._childNodesRestore(dt, idx.row, idx.column); }); } }, /** * Set the a column's visibility, taking into account multiple rows * in a header / footer and colspan attributes * @param {*} col * @param {*} showHide * @param {*} structure */ _setHeaderVis: function (col, showHide, structure) { var that = this; var display = showHide ? '' : 'none'; structure.forEach(function (row) { if (row[col]) { $(row[col].cell) .css('display', display) .toggleClass('dtr-hidden', !showHide); } else { // In a colspan - need to rewind calc the new span since // display:none elements do not count as being spanned over var search = col; while (search >= 0) { if (row[search]) { row[search].cell.colSpan = that._colspan(row, search); break; } search--; } } }); }, /** * How many columns should this cell span * * @param {*} row Header structure row * @param {*} idx The column index of the cell to span */ _colspan: function (row, idx) { var colspan = 1; for (var col = idx + 1; col < row.length; col++) { if (row[col] === null && this.s.current[col]) { // colspan and not hidden by Responsive colspan++; } else if (row[col]) { // Got the next cell, jump out break; } } return colspan; }, /** * Update the cell tab indexes for keyboard accessibility. This is called on * every table draw - that is potentially inefficient, but also the least * complex option given that column visibility can change on the fly. Its a * shame user-focus was removed from CSS 3 UI, as it would have solved this * issue with a single CSS statement. * * @private */ _tabIndexes: function () { var dt = this.s.dt; var cells = dt.cells({ page: 'current' }).nodes().to$(); var ctx = dt.settings()[0]; var target = this.c.details.target; cells.filter('[data-dtr-keyboard]').removeData('[data-dtr-keyboard]'); if (typeof target === 'number') { dt.cells(null, target, { page: 'current' }) .nodes() .to$() .attr('tabIndex', ctx.iTabIndex) .data('dtr-keyboard', 1); } else { // This is a bit of a hack - we need to limit the selected nodes to just // those of this table if (target === 'td:first-child, th:first-child') { target = '>td:first-child, >th:first-child'; } $(target, dt.rows({ page: 'current' }).nodes()) .attr('tabIndex', ctx.iTabIndex) .data('dtr-keyboard', 1); } } }); /** * List of default breakpoints. Each item in the array is an object with two * properties: * * * `name` - the breakpoint name. * * `width` - the breakpoint width * * @name Responsive.breakpoints * @static */ Responsive.breakpoints = [ { name: 'desktop', width: Infinity }, { name: 'tablet-l', width: 1024 }, { name: 'tablet-p', width: 768 }, { name: 'mobile-l', width: 480 }, { name: 'mobile-p', width: 320 } ]; /** * Display methods - functions which define how the hidden data should be shown * in the table. * * @namespace * @name Responsive.defaults * @static */ Responsive.display = { childRow: function (row, update, render) { var rowNode = $(row.node()); if (update) { if (rowNode.hasClass('dtr-expanded')) { row.child(render(), 'child').show(); return true; } } else { if (!rowNode.hasClass('dtr-expanded')) { var rendered = render(); if (rendered === false) { return false; } row.child(rendered, 'child').show(); return true; } else { row.child(false); return false; } } }, childRowImmediate: function (row, update, render) { var rowNode = $(row.node()); if ( (!update && rowNode.hasClass('dtr-expanded')) || !row.responsive.hasHidden() ) { // User interaction and the row is show, or nothing to show row.child(false); return false; } else { // Display var rendered = render(); if (rendered === false) { return false; } row.child(rendered, 'child').show(); return true; } }, // This is a wrapper so the modal options for Bootstrap and jQuery UI can // have options passed into them. This specific one doesn't need to be a // function but it is for consistency in the `modal` name modal: function (options) { return function (row, update, render, closeCallback) { var modal; var rendered = render(); if (rendered === false) { return false; } if (!update) { // Show a modal var close = function () { modal.remove(); // will tidy events for us $(document).off('keypress.dtr'); $(row.node()).removeClass('dtr-expanded'); closeCallback(); }; modal = $('
') .append( $('
') .append( $('
') .data('dtr-row-idx', row.index()) .append(rendered) ) .append( $( '
×
' ).click(function () { close(); }) ) ) .append( $('
').click( function () { close(); } ) ) .appendTo('body'); $(row.node()).addClass('dtr-expanded'); $(document).on('keyup.dtr', function (e) { if (e.keyCode === 27) { e.stopPropagation(); close(); } }); } else { modal = $('div.dtr-modal-content'); if (modal.length && row.index() === modal.data('dtr-row-idx')) { modal.empty().append(rendered); } else { // Modal not shown, nothing to update return null; } } if (options && options.header) { $('div.dtr-modal-content').prepend( '

' + options.header(row) + '

' ); } return true; }; } }; /** * Display methods - functions which define how the hidden data should be shown * in the table. * * @namespace * @name Responsive.defaults * @static */ Responsive.renderer = { listHiddenNodes: function () { return function (api, rowIdx, columns) { var that = this; var ul = $( '
    ' ); var found = false; $.each(columns, function (i, col) { if (col.hidden) { var klass = col.className ? 'class="' + col.className + '"' : ''; $( '
  • ' + '' + col.title + ' ' + '
  • ' ) .append( $('').append( that._childNodes( api, col.rowIndex, col.columnIndex ) ) ) // api.cell( col.rowIndex, col.columnIndex ).node().childNodes ) ) .appendTo(ul); found = true; } }); return found ? ul : false; }; }, listHidden: function () { return function (api, rowIdx, columns) { var data = $.map(columns, function (col) { var klass = col.className ? 'class="' + col.className + '"' : ''; return col.hidden ? '
  • ' + '' + col.title + ' ' + '' + col.data + '' + '
  • ' : ''; }).join(''); return data ? $( '
      ' ).append(data) : false; }; }, tableAll: function (options) { options = $.extend( { tableClass: '' }, options ); return function (api, rowIdx, columns) { var data = $.map(columns, function (col) { var klass = col.className ? 'class="' + col.className + '"' : ''; return ( '' + '' + col.title + ':' + ' ' + '' + col.data + '' + '' ); }).join(''); return $( '' ).append(data); }; } }; /** * Responsive default settings for initialisation * * @namespace * @name Responsive.defaults * @static */ Responsive.defaults = { /** * List of breakpoints for the instance. Note that this means that each * instance can have its own breakpoints. Additionally, the breakpoints * cannot be changed once an instance has been creased. * * @type {Array} * @default Takes the value of `Responsive.breakpoints` */ breakpoints: Responsive.breakpoints, /** * Enable / disable auto hiding calculations. It can help to increase * performance slightly if you disable this option, but all columns would * need to have breakpoint classes assigned to them * * @type {Boolean} * @default `true` */ auto: true, /** * Details control. If given as a string value, the `type` property of the * default object is set to that value, and the defaults used for the rest * of the object - this is for ease of implementation. * * The object consists of the following properties: * * * `display` - A function that is used to show and hide the hidden details * * `renderer` - function that is called for display of the child row data. * The default function will show the data from the hidden columns * * `target` - Used as the selector for what objects to attach the child * open / close to * * `type` - `false` to disable the details display, `inline` or `column` * for the two control types * * @type {Object|string} */ details: { display: Responsive.display.childRow, renderer: Responsive.renderer.listHidden(), target: 0, type: 'inline' }, /** * Orthogonal data request option. This is used to define the data type * requested when Responsive gets the data to show in the child row. * * @type {String} */ orthogonal: 'display' }; /* * API */ var Api = $.fn.dataTable.Api; // Doesn't do anything - work around for a bug in DT... Not documented Api.register('responsive()', function () { return this; }); Api.register('responsive.index()', function (li) { li = $(li); return { column: li.data('dtr-index'), row: li.parent().data('dtr-index') }; }); Api.register('responsive.rebuild()', function () { return this.iterator('table', function (ctx) { if (ctx._responsive) { ctx._responsive._classLogic(); } }); }); Api.register('responsive.recalc()', function () { return this.iterator('table', function (ctx) { if (ctx._responsive) { ctx._responsive._resizeAuto(); ctx._responsive._resize(); } }); }); Api.register('responsive.hasHidden()', function () { var ctx = this.context[0]; return ctx._responsive ? $.inArray(false, ctx._responsive._responsiveOnlyHidden()) !== -1 : false; }); Api.registerPlural( 'columns().responsiveHidden()', 'column().responsiveHidden()', function () { return this.iterator( 'column', function (settings, column) { return settings._responsive ? settings._responsive._responsiveOnlyHidden()[column] : false; }, 1 ); } ); /** * Version information * * @name Responsive.version * @static */ Responsive.version = '3.0.2'; $.fn.dataTable.Responsive = Responsive; $.fn.DataTable.Responsive = Responsive; // Attach a listener to the document which listens for DataTables initialisation // events so we can automatically initialise $(document).on('preInit.dt.dtr', function (e, settings, json) { if (e.namespace !== 'dt') { return; } if ( $(settings.nTable).hasClass('responsive') || $(settings.nTable).hasClass('dt-responsive') || settings.oInit.responsive || DataTable.defaults.responsive ) { var init = settings.oInit.responsive; if (init !== false) { new Responsive(settings, $.isPlainObject(init) ? init : {}); } } }); export default DataTable;