/*! Select for DataTables 1.2.2
* 2015-2016 SpryMedia Ltd - datatables.net/license/mit
*/
/**
* @summary Select for DataTables
* @description A collection of API methods, events and buttons for DataTables
* that provides selection options of the items in a DataTable
* @version 1.2.2
* @file dataTables.select.js
* @author SpryMedia Ltd (www.sprymedia.co.uk)
* @contact datatables.net/forums
* @copyright Copyright 2015-2016 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/extensions/select
*/
(function( factory ){
if ( typeof define === 'function' && define.amd ) {
// AMD
define( ['jquery', 'datatables.net'], function ( $ ) {
return factory( $, window, document );
} );
}
else if ( typeof exports === 'object' ) {
// CommonJS
module.exports = function (root, $) {
if ( ! root ) {
root = window;
}
if ( ! $ || ! $.fn.dataTable ) {
$ = require('datatables.net')(root, $).$;
}
return factory( $, root, root.document );
};
}
else {
// Browser
factory( jQuery, window, document );
}
}(function( $, window, document, undefined ) {
'use strict';
var DataTable = $.fn.dataTable;
// Version information for debugger
DataTable.select = {};
DataTable.select.version = '1.2.2';
DataTable.select.init = function ( dt ) {
var ctx = dt.settings()[0];
var init = ctx.oInit.select;
var defaults = DataTable.defaults.select;
var opts = init === undefined ?
defaults :
init;
// Set defaults
var items = 'row';
var style = 'api';
var blurable = false;
var info = true;
var selector = 'td, th';
var className = 'selected';
var setStyle = false;
ctx._select = {};
// Initialisation customisations
if ( opts === true ) {
style = 'os';
setStyle = true;
}
else if ( typeof opts === 'string' ) {
style = opts;
setStyle = true;
}
else if ( $.isPlainObject( opts ) ) {
if ( opts.blurable !== undefined ) {
blurable = opts.blurable;
}
if ( opts.info !== undefined ) {
info = opts.info;
}
if ( opts.items !== undefined ) {
items = opts.items;
}
if ( opts.style !== undefined ) {
style = opts.style;
setStyle = true;
}
if ( opts.selector !== undefined ) {
selector = opts.selector;
}
if ( opts.className !== undefined ) {
className = opts.className;
}
}
dt.select.selector( selector );
dt.select.items( items );
dt.select.style( style );
dt.select.blurable( blurable );
dt.select.info( info );
ctx._select.className = className;
// Sort table based on selected rows. Requires Select Datatables extension
$.fn.dataTable.ext.order['select-checkbox'] = function ( settings, col ) {
return this.api().column( col, {order: 'index'} ).nodes().map( function ( td ) {
if ( settings._select.items === 'row' ) {
return $( td ).parent().hasClass( settings._select.className );
} else if ( settings._select.items === 'cell' ) {
return $( td ).hasClass( settings._select.className );
}
return false;
});
};
// If the init options haven't enabled select, but there is a selectable
// class name, then enable
if ( ! setStyle && $( dt.table().node() ).hasClass( 'selectable' ) ) {
dt.select.style( 'os' );
}
};
/*
Select is a collection of API methods, event handlers, event emitters and
buttons (for the `Buttons` extension) for DataTables. It provides the following
features, with an overview of how they are implemented:
## Selection of rows, columns and cells. Whether an item is selected or not is
stored in:
* rows: a `_select_selected` property which contains a boolean value of the
DataTables' `aoData` object for each row
* columns: a `_select_selected` property which contains a boolean value of the
DataTables' `aoColumns` object for each column
* cells: a `_selected_cells` property which contains an array of boolean values
of the `aoData` object for each row. The array is the same length as the
columns array, with each element of it representing a cell.
This method of using boolean flags allows Select to operate when nodes have not
been created for rows / cells (DataTables' defer rendering feature).
## API methods
A range of API methods are available for triggering selection and de-selection
of rows. Methods are also available to configure the selection events that can
be triggered by an end user (such as which items are to be selected). To a large
extent, these of API methods *is* Select. It is basically a collection of helper
functions that can be used to select items in a DataTable.
Configuration of select is held in the object `_select` which is attached to the
DataTables settings object on initialisation. Select being available on a table
is not optional when Select is loaded, but its default is for selection only to
be available via the API - so the end user wouldn't be able to select rows
without additional configuration.
The `_select` object contains the following properties:
```
{
items:string - Can be `rows`, `columns` or `cells`. Defines what item
will be selected if the user is allowed to activate row
selection using the mouse.
style:string - Can be `none`, `single`, `multi` or `os`. Defines the
interaction style when selecting items
blurable:boolean - If row selection can be cleared by clicking outside of
the table
info:boolean - If the selection summary should be shown in the table
information elements
}
```
In addition to the API methods, Select also extends the DataTables selector
options for rows, columns and cells adding a `selected` option to the selector
options object, allowing the developer to select only selected items or
unselected items.
## Mouse selection of items
Clicking on items can be used to select items. This is done by a simple event
handler that will select the items using the API methods.
*/
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* Local functions
*/
/**
* Add one or more cells to the selection when shift clicking in OS selection
* style cell selection.
*
* Cell range is more complicated than row and column as we want to select
* in the visible grid rather than by index in sequence. For example, if you
* click first in cell 1-1 and then shift click in 2-2 - cells 1-2 and 2-1
* should also be selected (and not 1-3, 1-4. etc)
*
* @param {DataTable.Api} dt DataTable
* @param {object} idx Cell index to select to
* @param {object} last Cell index to select from
* @private
*/
function cellRange( dt, idx, last )
{
var indexes;
var columnIndexes;
var rowIndexes;
var selectColumns = function ( start, end ) {
if ( start > end ) {
var tmp = end;
end = start;
start = tmp;
}
var record = false;
return dt.columns( ':visible' ).indexes().filter( function (i) {
if ( i === start ) {
record = true;
}
if ( i === end ) { // not else if, as start might === end
record = false;
return true;
}
return record;
} );
};
var selectRows = function ( start, end ) {
var indexes = dt.rows( { search: 'applied' } ).indexes();
// Which comes first - might need to swap
if ( indexes.indexOf( start ) > indexes.indexOf( end ) ) {
var tmp = end;
end = start;
start = tmp;
}
var record = false;
return indexes.filter( function (i) {
if ( i === start ) {
record = true;
}
if ( i === end ) {
record = false;
return true;
}
return record;
} );
};
if ( ! dt.cells( { selected: true } ).any() && ! last ) {
// select from the top left cell to this one
columnIndexes = selectColumns( 0, idx.column );
rowIndexes = selectRows( 0 , idx.row );
}
else {
// Get column indexes between old and new
columnIndexes = selectColumns( last.column, idx.column );
rowIndexes = selectRows( last.row , idx.row );
}
indexes = dt.cells( rowIndexes, columnIndexes ).flatten();
if ( ! dt.cells( idx, { selected: true } ).any() ) {
// Select range
dt.cells( indexes ).select();
}
else {
// Deselect range
dt.cells( indexes ).deselect();
}
}
/**
* Disable mouse selection by removing the selectors
*
* @param {DataTable.Api} dt DataTable to remove events from
* @private
*/
function disableMouseSelection( dt )
{
var ctx = dt.settings()[0];
var selector = ctx._select.selector;
$( dt.table().container() )
.off( 'mousedown.dtSelect', selector )
.off( 'mouseup.dtSelect', selector )
.off( 'click.dtSelect', selector );
$('body').off( 'click.dtSelect' + dt.table().node().id );
}
/**
* Attach mouse listeners to the table to allow mouse selection of items
*
* @param {DataTable.Api} dt DataTable to remove events from
* @private
*/
function enableMouseSelection ( dt )
{
var container = $( dt.table().container() );
var ctx = dt.settings()[0];
var selector = ctx._select.selector;
container
.on( 'mousedown.dtSelect', selector, function(e) {
// Disallow text selection for shift clicking on the table so multi
// element selection doesn't look terrible!
if ( e.shiftKey || e.metaKey || e.ctrlKey ) {
container
.css( '-moz-user-select', 'none' )
.one('selectstart.dtSelect', selector, function () {
return false;
} );
}
} )
.on( 'mouseup.dtSelect', selector, function() {
// Allow text selection to occur again, Mozilla style (tested in FF
// 35.0.1 - still required)
container.css( '-moz-user-select', '' );
} )
.on( 'click.dtSelect', selector, function ( e ) {
var items = dt.select.items();
var idx;
// If text was selected (click and drag), then we shouldn't change
// the row's selected state
if ( window.getSelection && $.trim( window.getSelection().toString() ) ) {
return;
}
var ctx = dt.settings()[0];
// Ignore clicks inside a sub-table
if ( $(e.target).closest('div.dataTables_wrapper')[0] != dt.table().container() ) {
return;
}
var cell = dt.cell( $(e.target).closest('td, th') );
// Check the cell actually belongs to the host DataTable (so child
// rows, etc, are ignored)
if ( ! cell.any() ) {
return;
}
var event = $.Event('user-select.dt');
eventTrigger( dt, event, [ items, cell, e ] );
if ( event.isDefaultPrevented() ) {
return;
}
var cellIndex = cell.index();
if ( items === 'row' ) {
idx = cellIndex.row;
typeSelect( e, dt, ctx, 'row', idx );
}
else if ( items === 'column' ) {
idx = cell.index().column;
typeSelect( e, dt, ctx, 'column', idx );
}
else if ( items === 'cell' ) {
idx = cell.index();
typeSelect( e, dt, ctx, 'cell', idx );
}
ctx._select_lastCell = cellIndex;
} );
// Blurable
$('body').on( 'click.dtSelect' + dt.table().node().id, function ( e ) {
if ( ctx._select.blurable ) {
// If the click was inside the DataTables container, don't blur
if ( $(e.target).parents().filter( dt.table().container() ).length ) {
return;
}
// Ignore elements which have been removed from the DOM (i.e. paging
// buttons)
if ( e.target.getRootNode() !== document ) {
return;
}
// Don't blur in Editor form
if ( $(e.target).parents('div.DTE').length ) {
return;
}
clear( ctx, true );
}
} );
}
/**
* Trigger an event on a DataTable
*
* @param {DataTable.Api} api DataTable to trigger events on
* @param {boolean} selected true if selected, false if deselected
* @param {string} type Item type acting on
* @param {boolean} any Require that there are values before
* triggering
* @private
*/
function eventTrigger ( api, type, args, any )
{
if ( any && ! api.flatten().length ) {
return;
}
if ( typeof type === 'string' ) {
type = type +'.dt';
}
args.unshift( api );
$(api.table().node()).trigger( type, args );
}
/**
* Update the information element of the DataTable showing information about the
* items selected. This is done by adding tags to the existing text
*
* @param {DataTable.Api} api DataTable to update
* @private
*/
function info ( api )
{
var ctx = api.settings()[0];
if ( ! ctx._select.info || ! ctx.aanFeatures.i ) {
return;
}
var rows = api.rows( { selected: true } ).flatten().length;
var columns = api.columns( { selected: true } ).flatten().length;
var cells = api.cells( { selected: true } ).flatten().length;
var add = function ( el, name, num ) {
el.append( $('').append( api.i18n(
'select.'+name+'s',
{ _: '%d '+name+'s selected', 0: '', 1: '1 '+name+' selected' },
num
) ) );
};
// Internal knowledge of DataTables to loop over all information elements
$.each( ctx.aanFeatures.i, function ( i, el ) {
el = $(el);
var output = $('');
add( output, 'row', rows );
add( output, 'column', columns );
add( output, 'cell', cells );
var exisiting = el.children('span.select-info');
if ( exisiting.length ) {
exisiting.remove();
}
if ( output.text() !== '' ) {
el.append( output );
}
} );
}
/**
* Initialisation of a new table. Attach event handlers and callbacks to allow
* Select to operate correctly.
*
* This will occur _after_ the initial DataTables initialisation, although
* before Ajax data is rendered, if there is ajax data
*
* @param {DataTable.settings} ctx Settings object to operate on
* @private
*/
function init ( ctx ) {
var api = new DataTable.Api( ctx );
// Row callback so that classes can be added to rows and cells if the item
// was selected before the element was created. This will happen with the
// `deferRender` option enabled.
//
// This method of attaching to `aoRowCreatedCallback` is a hack until
// DataTables has proper events for row manipulation If you are reviewing
// this code to create your own plug-ins, please do not do this!
ctx.aoRowCreatedCallback.push( {
fn: function ( row, data, index ) {
var i, ien;
var d = ctx.aoData[ index ];
// Row
if ( d._select_selected ) {
$( row ).addClass( ctx._select.className );
}
// Cells and columns - if separated out, we would need to do two
// loops, so it makes sense to combine them into a single one
for ( i=0, ien=ctx.aoColumns.length ; i idx2 ) {
var tmp = idx2;
idx2 = idx1;
idx1 = tmp;
}
indexes.splice( idx2+1, indexes.length );
indexes.splice( 0, idx1 );
}
if ( ! dt[type]( idx, { selected: true } ).any() ) {
// Select range
dt[type+'s']( indexes ).select();
}
else {
// Deselect range - need to keep the clicked on row selected
indexes.splice( $.inArray( idx, indexes ), 1 );
dt[type+'s']( indexes ).deselect();
}
}
/**
* Clear all selected items
*
* @param {DataTable.settings} ctx Settings object of the host DataTable
* @param {boolean} [force=false] Force the de-selection to happen, regardless
* of selection style
* @private
*/
function clear( ctx, force )
{
if ( force || ctx._select.style === 'single' ) {
var api = new DataTable.Api( ctx );
api.rows( { selected: true } ).deselect();
api.columns( { selected: true } ).deselect();
api.cells( { selected: true } ).deselect();
}
}
/**
* Select items based on the current configuration for style and items.
*
* @param {object} e Mouse event object
* @param {DataTables.Api} dt DataTable
* @param {DataTable.settings} ctx Settings object of the host DataTable
* @param {string} type Items to select
* @param {int|object} idx Index of the item to select
* @private
*/
function typeSelect ( e, dt, ctx, type, idx )
{
var style = dt.select.style();
var isSelected = dt[type]( idx, { selected: true } ).any();
if ( style === 'os' ) {
if ( e.ctrlKey || e.metaKey ) {
// Add or remove from the selection
dt[type]( idx ).select( ! isSelected );
}
else if ( e.shiftKey ) {
if ( type === 'cell' ) {
cellRange( dt, idx, ctx._select_lastCell || null );
}
else {
rowColumnRange( dt, type, idx, ctx._select_lastCell ?
ctx._select_lastCell[type] :
null
);
}
}
else {
// No cmd or shift click - deselect if selected, or select
// this row only
var selected = dt[type+'s']( { selected: true } );
if ( isSelected && selected.flatten().length === 1 ) {
dt[type]( idx ).deselect();
}
else {
selected.deselect();
dt[type]( idx ).select();
}
}
} else if ( style == 'multi+shift' ) {
if ( e.shiftKey ) {
if ( type === 'cell' ) {
cellRange( dt, idx, ctx._select_lastCell || null );
}
else {
rowColumnRange( dt, type, idx, ctx._select_lastCell ?
ctx._select_lastCell[type] :
null
);
}
}
else {
dt[ type ]( idx ).select( ! isSelected );
}
}
else {
dt[ type ]( idx ).select( ! isSelected );
}
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* DataTables selectors
*/
// row and column are basically identical just assigned to different properties
// and checking a different array, so we can dynamically create the functions to
// reduce the code size
$.each( [
{ type: 'row', prop: 'aoData' },
{ type: 'column', prop: 'aoColumns' }
], function ( i, o ) {
DataTable.ext.selector[ o.type ].push( function ( settings, opts, indexes ) {
var selected = opts.selected;
var data;
var out = [];
if ( selected === undefined ) {
return indexes;
}
for ( var i=0, ien=indexes.length ; i 0 );
} );
this.disable();
},
destroy: function ( dt, node, config ) {
dt.off( config._eventNamespace );
}
}
} );
$.each( [ 'Row', 'Column', 'Cell' ], function ( i, item ) {
var lc = item.toLowerCase();
DataTable.ext.buttons[ 'select'+item+'s' ] = {
text: i18n( 'select'+item+'s', 'Select '+lc+'s' ),
className: 'buttons-select-'+lc+'s',
action: function () {
this.select.items( lc );
},
init: function ( dt ) {
var that = this;
dt.on( 'selectItems.dt.DT', function ( e, ctx, items ) {
that.active( items === lc );
} );
}
};
} );
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* Initialisation
*/
// DataTables creation - check if select has been defined in the options. Note
// this required that the table be in the document! If it isn't then something
// needs to trigger this method unfortunately. The next major release of
// DataTables will rework the events and address this.
$(document).on( 'preInit.dt.dtSelect', function (e, ctx) {
if ( e.namespace !== 'dt' ) {
return;
}
DataTable.select.init( new DataTable.Api( ctx ) );
} );
return DataTable.select;
}));