/*! Select for DataTables 1.0.1 * 2015 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.0.1 * @file dataTables.select.js * @author SpryMedia Ltd (www.sprymedia.co.uk) * @contact datatables.net/forums * @copyright Copyright 2015 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(window, document, undefined) { var factory = function( $, DataTable ) { "use strict"; // Version information for debugger DataTable.select = {}; DataTable.select.version = '1.0.1'; /* 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().body() ) .off( 'mousedown.dtSelect', selector ) .off( 'mouseup.dtSelect', selector ) .off( 'click.dtSelect', selector ); $('body').off( 'click.dtSelect' ); } /** * 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 body = $( dt.table().body() ); var ctx = dt.settings()[0]; var selector = ctx._select.selector; body .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 ) { body .css( '-moz-user-select', 'none' ) .one('selectstart.dtSelect', selector, function () { return false; } ); } } ) .on( 'mouseup.dtSelect', selector, function(e) { // Allow text selection to occur again, Mozilla style (tested in FF // 35.0.1 - still required) body.css( '-moz-user-select', '' ); } ) .on( 'click.dtSelect', selector, function ( e ) { var items = dt.select.items(); var cellIndex = dt.cell( this ).index(); var idx; var ctx = dt.settings()[0]; // Ignore clicks inside a sub-table if ( $(e.target).closest('tbody')[0] != body[0] ) { return; } // Check the cell actually belongs to the host DataTable (so child rows, // etc, are ignored) if ( ! dt.cell( e.target ).any() ) { return; } if ( items === 'row' ) { idx = cellIndex.row; typeSelect( e, dt, ctx, 'row', idx ); } else if ( items === 'column' ) { idx = dt.cell( e.target ).index().column; typeSelect( e, dt, ctx, 'column', idx ); } else if ( items === 'cell' ) { idx = dt.cell( e.target ).index(); typeSelect( e, dt, ctx, 'cell', idx ); } ctx._select_lastCell = cellIndex; } ); // Blurable $('body').on( 'click.dtSelect', 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; } // 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; } args.unshift( api ); $(api.table().node()).triggerHandler( type+'.dt', 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 output = $(''); var add = function ( name, num ) { output.append( $('').append( api.i18n( 'select.'+name+'s', { _: '%d '+name+'s selected', 0: '', 1: '1 '+name+' selected' }, num ) ) ); }; add( 'row', api.rows( { selected: true } ).flatten().length ); add( 'column', api.columns( { selected: true } ).flatten().length ); add( 'cell', api.cells( { selected: true } ).flatten().length ); // Internal knowledge of DataTables to loop over all information elements $.each( ctx.aanFeatures.i, function ( i, el ) { el = $(el); 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( 'selected' ); } // 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 { 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