package.js.dataTables.select.js Maven / Gradle / Ivy
The newest version!
/*! Select for DataTables 2.1.0
* © SpryMedia Ltd - datatables.net/license/mit
*/
(function( factory ){
if ( typeof define === 'function' && define.amd ) {
// AMD
define( ['jquery', 'datatables.net'], function ( $ ) {
return factory( $, window, document );
} );
}
else if ( typeof exports === 'object' ) {
// CommonJS
var jq = require('jquery');
var cjsRequires = function (root, $) {
if ( ! $.fn.dataTable ) {
require('datatables.net')(root, $);
}
};
if (typeof window === 'undefined') {
module.exports = function (root, $) {
if ( ! root ) {
// CommonJS environments without a window global must pass a
// root. This will give an error otherwise
root = window;
}
if ( ! $ ) {
$ = jq( root );
}
cjsRequires( root, $ );
return factory( $, root, root.document );
};
}
else {
cjsRequires( window, jq );
module.exports = factory( jq, window, window.document );
}
}
else {
// Browser
factory( jQuery, window, document );
}
}(function( $, window, document ) {
'use strict';
var DataTable = $.fn.dataTable;
// Version information for debugger
DataTable.select = {};
DataTable.select.classes = {
checkbox: 'dt-select-checkbox'
};
DataTable.select.version = '2.1.0';
DataTable.select.init = function (dt) {
var ctx = dt.settings()[0];
if (!DataTable.versionCheck('2')) {
throw 'Warning: Select requires DataTables 2 or newer';
}
if (ctx._select) {
return;
}
var savedSelected = dt.state.loaded();
var selectAndSave = function (e, settings, data) {
if (data === null || data.select === undefined) {
return;
}
// Clear any currently selected rows, before restoring state
// None will be selected on first initialisation
if (dt.rows({ selected: true }).any()) {
dt.rows().deselect();
}
if (data.select.rows !== undefined) {
dt.rows(data.select.rows).select();
}
if (dt.columns({ selected: true }).any()) {
dt.columns().deselect();
}
if (data.select.columns !== undefined) {
dt.columns(data.select.columns).select();
}
if (dt.cells({ selected: true }).any()) {
dt.cells().deselect();
}
if (data.select.cells !== undefined) {
for (var i = 0; i < data.select.cells.length; i++) {
dt.cell(data.select.cells[i].row, data.select.cells[i].column).select();
}
}
dt.state.save();
};
dt.on('stateSaveParams', function (e, settings, data) {
data.select = {};
data.select.rows = dt.rows({ selected: true }).ids(true).toArray();
data.select.columns = dt.columns({ selected: true })[0];
data.select.cells = dt.cells({ selected: true })[0].map(function (coords) {
return { row: dt.row(coords.row).id(true), column: coords.column };
});
})
.on('stateLoadParams', selectAndSave)
.one('init', function () {
selectAndSave(undefined, undefined, savedSelected);
});
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 toggleable = true;
var selectable = null;
var info = true;
var selector = 'td, th';
var className = 'selected';
var headerCheckbox = true;
var setStyle = false;
ctx._select = {
infoEls: []
};
// 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.toggleable !== undefined) {
toggleable = opts.toggleable;
}
if (opts.info !== undefined) {
info = opts.info;
}
if (opts.items !== undefined) {
items = opts.items;
}
if (opts.style !== undefined) {
style = opts.style;
setStyle = true;
}
else {
style = 'os';
setStyle = true;
}
if (opts.selector !== undefined) {
selector = opts.selector;
}
if (opts.className !== undefined) {
className = opts.className;
}
if (opts.headerCheckbox !== undefined) {
headerCheckbox = opts.headerCheckbox;
}
if (opts.selectable !== undefined) {
selectable = opts.selectable;
}
}
dt.select.selector(selector);
dt.select.items(items);
dt.select.style(style);
dt.select.blurable(blurable);
dt.select.toggleable(toggleable);
dt.select.info(info);
dt.select.selectable(selectable);
ctx._select.className = className;
// 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');
}
// Insert a checkbox into the header if needed - might need to wait
// for init complete
if (headerCheckbox || headerCheckbox === 'select-page' || headerCheckbox === 'select-all') {
dt.ready(function () {
initCheckboxHeader(dt, headerCheckbox);
});
}
};
/*
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
toggleable:boolean - If row selection can be cancelled by repeated clicking
on the row
info:boolean - If the selection summary should be shown in the table
information elements
infoEls:element[] - List of HTML elements with info elements for a table
}
```
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();
}
}
/**
* Get the class
* @returns
*/
function checkboxClass(selector) {
var name = DataTable.select.classes.checkbox;
return selector
? name.replace(/ /g, '.')
: name;
}
/**
* 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' + _safeId(dt.table().node()));
}
/**
* 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;
var matchSelection;
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;
});
}
if (window.getSelection) {
matchSelection = window.getSelection();
}
})
.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 (matchSelection) {
var selection = window.getSelection();
// If the element that contains the selection is not in the table, we can ignore it
// This can happen if the developer selects text from the click event
if (
!selection.anchorNode ||
$(selection.anchorNode).closest('table')[0] === dt.table().node()
) {
if (selection !== matchSelection) {
return;
}
}
}
var ctx = dt.settings()[0];
var container = dt.table().container();
// Ignore clicks inside a sub-table
if ($(e.target).closest('div.dt-container')[0] != 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' + _safeId(dt.table().node()), 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).parents('html').length === 0) {
return;
}
// Don't blur in Editor form
if ($(e.target).parents('div.DTE').length) {
return;
}
var event = $.Event('select-blur.dt');
eventTrigger(dt, event, [e.target, e]);
if (event.isDefaultPrevented()) {
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);
}
/**
* Determine if a column is a checkbox column
* @param {*} col DataTables column object
* @returns
*/
function isCheckboxColumn(col) {
return col.mRender && col.mRender._name === 'selectCheckbox';
}
/**
* 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, node) {
if (api.select.style() === 'api' || api.select.info() === false) {
return;
}
// If _select_set has any length, then ids are available and should be used
// as the counter. Otherwise use the API to workout how many rows are
// selected.
var rowSetLength = api.settings()[0]._select_set.length;
var rows = rowSetLength ? rowSetLength : api.rows({ selected: true }).count();
var columns = api.columns({ selected: true }).count();
var cells = api.cells({ selected: true }).count();
var add = function (el, name, num) {
el.append(
$('').append(
api.i18n(
'select.' + name + 's',
{ _: '%d ' + name + 's selected', 0: '', 1: '1 ' + name + ' selected' },
num
)
)
);
};
var el = $(node);
var output = $('');
add(output, 'row', rows);
add(output, 'column', columns);
add(output, 'cell', cells);
var existing = el.children('span.select-info');
if (existing.length) {
existing.remove();
}
if (output.text() !== '') {
el.append(output);
}
}
/**
* Add a checkbox to the header for checkbox columns, allowing all rows to
* be selected, deselected or just to show the state.
*
* @param {*} dt API
* @param {*} headerCheckbox the header checkbox option
*/
function initCheckboxHeader( dt, headerCheckbox ) {
var dtSettings = dt.settings()[0];
var dtInternalColumns = dtSettings.aoColumns;
// Find any checkbox column(s)
dt.columns().iterator('column', function (s, idx) {
var col = dtInternalColumns[idx];
// Checkbox columns have a rendering function with a given name
if (! isCheckboxColumn(col)) {
return;
}
var header = dt.column(idx).header();
if (! $('input', header).length) {
// If no checkbox yet, insert one
var input = $('')
.attr({
class: checkboxClass(true),
type: 'checkbox',
'aria-label': dt.i18n('select.aria.headerCheckbox') || 'Select all rows'
})
.appendTo(header)
.on('change', function () {
if (this.checked) {
if (headerCheckbox == 'select-page') {
dt.rows({page: 'current'}).select();
} else {
dt.rows({search: 'applied'}).select();
}
}
else {
if (headerCheckbox == 'select-page') {
dt.rows({page: 'current', selected: true}).deselect();
}
else {
dt.rows({selected: true}).deselect();
}
}
})
.on('click', function (e) {
e.stopPropagation();
});
// Update the header checkbox's state when the selection in the
// table changes
dt.on('draw select deselect', function (e, pass, type) {
if (type === 'row' || ! type) {
var nums = headerCheckboxState(dt, headerCheckbox);
if (nums.search && nums.search <= nums.count && nums.search === nums.available) {
input
.prop('checked', true)
.prop('indeterminate', false);
}
else if (nums.search === 0 && nums.count === 0) {
input
.prop('checked', false)
.prop('indeterminate', false);
}
else {
input
.prop('checked', false)
.prop('indeterminate', true);
}
}
});
}
});
}
/**
* Determine the counts used to define the header checkbox's state
*
* @param {*} dt DT API
* @param {*} headerCheckbox Configuration for what the header checkbox does
* @returns Counts object
*/
function headerCheckboxState(dt, headerCheckbox) {
var ctx = dt.settings()[0];
var selectable = ctx._select.selectable;
var available = 0;
var count = headerCheckbox == 'select-page'
? dt.rows({page: 'current', selected: true}).count()
: dt.rows({selected: true}).count();
var search = headerCheckbox == 'select-page'
? dt.rows({page: 'current', selected: true}).count()
: dt.rows({search: 'applied', selected: true}).count();
if (! selectable) {
available = headerCheckbox == 'select-page'
? dt.rows({page: 'current'}).count()
: dt.rows({search: 'applied'}).count();
}
else {
// Need to count how many rows are actually selectable to know if all selectable
// rows are selected or not
var indexes = headerCheckbox == 'select-page'
? dt.rows({page: 'current'}).indexes()
: dt.rows({search: 'applied'}).indexes();
for (var i=0 ; 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(indexes.indexOf(idx), 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 toggleable = dt.select.toggleable();
var isSelected = dt[type](idx, { selected: true }).any();
if (isSelected && !toggleable) {
return;
}
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);
}
}
function _safeId(node) {
return node.id.replace(/[^a-zA-Z0-9\-\_]/g, '-');
}
/**
* Set up event handlers for cumulative selection
*
* @param {*} api DT API instance
*/
function _cumulativeEvents(api) {
// Add event listeners to add / remove from the _select_set
api.on('select', function (e, dt, type, indexes) {
// Only support for rows at the moment
if (type !== 'row') {
return;
}
var ctx = api.settings()[0];
_add(api, ctx._select_set, indexes);
});
api.on('deselect', function (e, dt, type, indexes) {
// Only support for rows at the moment
if (type !== 'row') {
return;
}
var ctx = api.settings()[0];
_remove(api, ctx._select_set, indexes);
});
}
function _add(api, arr, indexes) {
for (var i=0 ; i 0);
});
this.disable();
},
destroy: function (dt, node, config) {
dt.off(config._eventNamespace);
}
},
showSelected: {
text: i18n('showSelected', 'Show only selected'),
className: 'buttons-show-selected',
action: function (e, dt) {
if (dt.search.fixed('dt-select')) {
// Remove existing function
dt.search.fixed('dt-select', null);
this.active(false);
}
else {
// Use a fixed filtering function to match on selected rows
// This needs to reference the internal aoData since that is
// where Select stores its reference for the selected state
var dataSrc = dt.settings()[0].aoData;
dt.search.fixed('dt-select', function (text, data, idx) {
// _select_selected is set by Select on the data object for the row
return dataSrc[idx]._select_selected;
});
this.active(true);
}
dt.draw();
}
}
});
$.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;
this.active(dt.select.items() === lc);
dt.on('selectItems.dt.DT', function (e, ctx, items) {
that.active(items === lc);
});
}
};
});
// Note that DataTables 2.1 has more robust type detection, but we retain
// backwards compatibility with 2.0 for the moment.
DataTable.type('select-checkbox', {
className: 'dt-select',
detect: DataTable.versionCheck('2.1')
? {
oneOf: function () {
return false; // no op
},
allOf: function () {
return false; // no op
},
init: function (settings, col, idx) {
return isCheckboxColumn(col);
}
}
: function (data) {
// Rendering function will tell us if it is a checkbox type
return data === 'select-checkbox' ? data : false;
},
order: {
pre: function (d) {
return d === 'X' ? -1 : 0;
}
}
});
$.extend(true, DataTable.defaults.oLanguage, {
select: {
aria: {
rowCheckbox: 'Select row'
}
}
});
DataTable.render.select = function (valueProp, nameProp) {
var valueFn = valueProp ? DataTable.util.get(valueProp) : null;
var nameFn = nameProp ? DataTable.util.get(nameProp) : null;
var fn = function (data, type, row, meta) {
var dtRow = meta.settings.aoData[meta.row];
var selected = dtRow._select_selected;
var ariaLabel = meta.settings.oLanguage.select.aria.rowCheckbox;
var selectable = meta.settings._select.selectable;
if (type === 'display') {
// Check if the row is selectable before showing the checkbox
if (selectable) {
var result = selectable(row, dtRow.nTr, meta.row);
if (result === false) {
return '';
}
}
return $('')
.attr({
'aria-label': ariaLabel,
class: checkboxClass(),
name: nameFn ? nameFn(row) : null,
type: 'checkbox',
value: valueFn ? valueFn(row) : null,
checked: selected
})
.on('input', function (e) {
// Let Select 100% control the state of the checkbox
e.preventDefault();
// And make sure this checkbox matches it's row as it is possible
// to check out of sync if this was clicked on to deselect a range
// but remains selected itself
this.checked = $(this).closest('tr').hasClass('selected');
})[0];
}
else if (type === 'type') {
return 'select-checkbox';
}
else if (type === 'filter') {
return '';
}
return selected ? 'X' : '';
}
// Workaround so uglify doesn't strip the function name. It is used
// for the column type detection.
fn._name = 'selectCheckbox';
return fn;
}
// Legacy checkbox ordering
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).toString();
}
else if (settings._select.items === 'cell') {
return $(td).hasClass(settings._select.className).toString();
}
return false;
});
};
$.fn.DataTable.select = DataTable.select;
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* Initialisation
*/
// DataTables creation - we need this to run _before_ data is read in, but
// for backwards compat. we also run again on preInit. If it happens twice
// it will simply do nothing the second time around.
$(document).on('i18n.dt.dtSelect preInit.dt.dtSelect', function (e, ctx) {
if (e.namespace !== 'dt') {
return;
}
DataTable.select.init(new DataTable.Api(ctx));
});
return DataTable;
}));