/*! 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); }); $('