Show More
Commit Description:
announcement
Commit Description:
announcement
References:
File last commit:
Show/Diff file:
Action:
vendor/javascript/datatables/KeyTable-2.7.0/js/dataTables.keyTable.js
| 1361 lines
| 31.3 KiB
| application/javascript
| JavascriptLexer
|
r866 | /*! KeyTable 2.7.0 | |||
* ©2009-2022 SpryMedia Ltd - datatables.net/license | ||||
*/ | ||||
/** | ||||
* @summary KeyTable | ||||
* @description Spreadsheet like keyboard navigation for DataTables | ||||
* @version 2.7.0 | ||||
* @author SpryMedia Ltd (www.sprymedia.co.uk) | ||||
* @contact www.sprymedia.co.uk | ||||
* @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 | ||||
*/ | ||||
(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; | ||||
var namespaceCounter = 0; | ||||
var editorNamespaceCounter = 0; | ||||
var KeyTable = function ( dt, opts ) { | ||||
// Sanity check that we are using DataTables 1.10 or newer | ||||
if ( ! DataTable.versionCheck || ! DataTable.versionCheck( '1.10.8' ) ) { | ||||
throw 'KeyTable requires DataTables 1.10.8 or newer'; | ||||
} | ||||
// User and defaults configuration object | ||||
this.c = $.extend( true, {}, | ||||
DataTable.defaults.keyTable, | ||||
KeyTable.defaults, | ||||
opts | ||||
); | ||||
// Internal settings | ||||
this.s = { | ||||
/** @type {DataTable.Api} DataTables' API instance */ | ||||
dt: new DataTable.Api( dt ), | ||||
enable: true, | ||||
/** @type {bool} Flag for if a draw is triggered by focus */ | ||||
focusDraw: false, | ||||
/** @type {bool} Flag to indicate when waiting for a draw to happen. | ||||
* Will ignore key presses at this point | ||||
*/ | ||||
waitingForDraw: false, | ||||
/** @type {object} Information about the last cell that was focused */ | ||||
lastFocus: null, | ||||
/** @type {string} Unique namespace per instance */ | ||||
namespace: '.keyTable-'+(namespaceCounter++), | ||||
/** @type {Node} Input element for tabbing into the table */ | ||||
tabInput: null | ||||
}; | ||||
// DOM items | ||||
this.dom = { | ||||
}; | ||||
// Check if row reorder has already been initialised on this table | ||||
var settings = this.s.dt.settings()[0]; | ||||
var exisiting = settings.keytable; | ||||
if ( exisiting ) { | ||||
return exisiting; | ||||
} | ||||
settings.keytable = this; | ||||
this._constructor(); | ||||
}; | ||||
$.extend( KeyTable.prototype, { | ||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * | ||||
* API methods for DataTables API interface | ||||
*/ | ||||
/** | ||||
* Blur the table's cell focus | ||||
*/ | ||||
blur: function () | ||||
{ | ||||
this._blur(); | ||||
}, | ||||
/** | ||||
* Enable cell focus for the table | ||||
* | ||||
* @param {string} state Can be `true`, `false` or `-string navigation-only` | ||||
*/ | ||||
enable: function ( state ) | ||||
{ | ||||
this.s.enable = state; | ||||
}, | ||||
/** | ||||
* Get enable status | ||||
*/ | ||||
enabled: function () { | ||||
return this.s.enable; | ||||
}, | ||||
/** | ||||
* Focus on a cell | ||||
* @param {integer} row Row index | ||||
* @param {integer} column Column index | ||||
*/ | ||||
focus: function ( row, column ) | ||||
{ | ||||
this._focus( this.s.dt.cell( row, column ) ); | ||||
}, | ||||
/** | ||||
* Is the cell focused | ||||
* @param {object} cell Cell index to check | ||||
* @returns {boolean} true if focused, false otherwise | ||||
*/ | ||||
focused: function ( cell ) | ||||
{ | ||||
var lastFocus = this.s.lastFocus; | ||||
if ( ! lastFocus ) { | ||||
return false; | ||||
} | ||||
var lastIdx = this.s.lastFocus.cell.index(); | ||||
return cell.row === lastIdx.row && cell.column === lastIdx.column; | ||||
}, | ||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * | ||||
* Constructor | ||||
*/ | ||||
/** | ||||
* Initialise the KeyTable instance | ||||
* | ||||
* @private | ||||
*/ | ||||
_constructor: function () | ||||
{ | ||||
this._tabInput(); | ||||
var that = this; | ||||
var dt = this.s.dt; | ||||
var table = $( dt.table().node() ); | ||||
var namespace = this.s.namespace; | ||||
var editorBlock = false; | ||||
// Need to be able to calculate the cell positions relative to the table | ||||
if ( table.css('position') === 'static' ) { | ||||
table.css( 'position', 'relative' ); | ||||
} | ||||
// Click to focus | ||||
$( dt.table().body() ).on( 'click'+namespace, 'th, td', function (e) { | ||||
if ( that.s.enable === false ) { | ||||
return; | ||||
} | ||||
var cell = dt.cell( this ); | ||||
if ( ! cell.any() ) { | ||||
return; | ||||
} | ||||
that._focus( cell, null, false, e ); | ||||
} ); | ||||
// Key events | ||||
$( document ).on( 'keydown'+namespace, function (e) { | ||||
if ( ! editorBlock ) { | ||||
that._key( e ); | ||||
} | ||||
} ); | ||||
// Click blur | ||||
if ( this.c.blurable ) { | ||||
$( document ).on( 'mousedown'+namespace, function ( e ) { | ||||
// Click on the search input will blur focus | ||||
if ( $(e.target).parents( '.dataTables_filter' ).length ) { | ||||
that._blur(); | ||||
} | ||||
// 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; | ||||
} | ||||
// Or an Editor date input | ||||
if ( | ||||
$(e.target).parents('div.editor-datetime').length || | ||||
$(e.target).parents('div.dt-datetime').length | ||||
) { | ||||
return; | ||||
} | ||||
//If the click was inside the fixed columns container, don't blur | ||||
if ( $(e.target).parents().filter('.DTFC_Cloned').length ) { | ||||
return; | ||||
} | ||||
that._blur(); | ||||
} ); | ||||
} | ||||
if ( this.c.editor ) { | ||||
var editor = this.c.editor; | ||||
// Need to disable KeyTable when the main editor is shown | ||||
editor.on( 'open.keyTableMain', function (e, mode, action) { | ||||
if ( mode !== 'inline' && that.s.enable ) { | ||||
that.enable( false ); | ||||
editor.one( 'close'+namespace, function () { | ||||
that.enable( true ); | ||||
} ); | ||||
} | ||||
} ); | ||||
if ( this.c.editOnFocus ) { | ||||
dt.on( 'key-focus'+namespace+' key-refocus'+namespace, function ( e, dt, cell, orig ) { | ||||
that._editor( null, orig, true ); | ||||
} ); | ||||
} | ||||
// Activate Editor when a key is pressed (will be ignored, if | ||||
// already active). | ||||
dt.on( 'key'+namespace, function ( e, dt, key, cell, orig ) { | ||||
that._editor( key, orig, false ); | ||||
} ); | ||||
// Active editing on double click - it will already have focus from | ||||
// the click event handler above | ||||
$( dt.table().body() ).on( 'dblclick'+namespace, 'th, td', function (e) { | ||||
if ( that.s.enable === false ) { | ||||
return; | ||||
} | ||||
var cell = dt.cell( this ); | ||||
if ( ! cell.any() ) { | ||||
return; | ||||
} | ||||
if ( that.s.lastFocus && this !== that.s.lastFocus.cell.node() ) { | ||||
return; | ||||
} | ||||
that._editor( null, e, true ); | ||||
} ); | ||||
// While Editor is busy processing, we don't want to process any key events | ||||
editor | ||||
.on('preSubmit', function () { | ||||
editorBlock = true; | ||||
} ) | ||||
.on('preSubmitCancelled', function () { | ||||
editorBlock = false; | ||||
} ) | ||||
.on('submitComplete', function () { | ||||
editorBlock = false; | ||||
} ); | ||||
} | ||||
// Stave saving | ||||
// if ( dt.settings()[0].oFeatures.bStateSave ) { | ||||
dt.on( 'stateSaveParams'+namespace, function (e, s, d) { | ||||
d.keyTable = that.s.lastFocus ? | ||||
that.s.lastFocus.cell.index() : | ||||
null; | ||||
} ); | ||||
// } | ||||
dt.on( 'column-visibility'+namespace, function (e) { | ||||
that._tabInput(); | ||||
} ); | ||||
dt.on( 'column-reorder'+namespace, function (e, s, d) { | ||||
// Need to update the last focus cell's index | ||||
var lastFocus = that.s.lastFocus; | ||||
if (lastFocus && lastFocus.cell) { | ||||
var curr = lastFocus.relative.column; | ||||
// Manipulate the API instance to correct the column index | ||||
lastFocus.cell[0][0].column = d.mapping.indexOf(curr); | ||||
lastFocus.relative.column = d.mapping.indexOf(curr); | ||||
} | ||||
} ); | ||||
// Redraw - retain focus on the current cell | ||||
dt.on( 'draw'+namespace, function (e) { | ||||
that._tabInput(); | ||||
if ( that.s.focusDraw ) { | ||||
return; | ||||
} | ||||
var lastFocus = that.s.lastFocus; | ||||
if ( lastFocus ) { | ||||
var relative = that.s.lastFocus.relative; | ||||
var info = dt.page.info(); | ||||
var row = relative.row + info.start; | ||||
if ( info.recordsDisplay === 0 ) { | ||||
return; | ||||
} | ||||
// Reverse if needed | ||||
if ( row >= info.recordsDisplay ) { | ||||
row = info.recordsDisplay - 1; | ||||
} | ||||
that._focus( row, relative.column, true, e ); | ||||
} | ||||
} ); | ||||
// Clipboard support | ||||
if ( this.c.clipboard ) { | ||||
this._clipboard(); | ||||
} | ||||
dt.on( 'destroy'+namespace, function () { | ||||
that._blur( true ); | ||||
// Event tidy up | ||||
dt.off( namespace ); | ||||
$( dt.table().body() ) | ||||
.off( 'click'+namespace, 'th, td' ) | ||||
.off( 'dblclick'+namespace, 'th, td' ); | ||||
$( document ) | ||||
.off( 'mousedown'+namespace ) | ||||
.off( 'keydown'+namespace ) | ||||
.off( 'copy'+namespace ) | ||||
.off( 'paste'+namespace ); | ||||
} ); | ||||
// Initial focus comes from state or options | ||||
var state = dt.state.loaded(); | ||||
if ( state && state.keyTable ) { | ||||
// Wait until init is done | ||||
dt.one( 'init', function () { | ||||
var cell = dt.cell( state.keyTable ); | ||||
// Ensure that the saved cell still exists | ||||
if ( cell.any() ) { | ||||
cell.focus(); | ||||
} | ||||
} ); | ||||
} | ||||
else if ( this.c.focus ) { | ||||
dt.cell( this.c.focus ).focus(); | ||||
} | ||||
}, | ||||
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * | ||||
* Private methods | ||||
*/ | ||||
/** | ||||
* Blur the control | ||||
* | ||||
* @param {boolean} [noEvents=false] Don't trigger updates / events (for destroying) | ||||
* @private | ||||
*/ | ||||
_blur: function (noEvents) | ||||
{ | ||||
if ( ! this.s.enable || ! this.s.lastFocus ) { | ||||
return; | ||||
} | ||||
var cell = this.s.lastFocus.cell; | ||||
$( cell.node() ).removeClass( this.c.className ); | ||||
this.s.lastFocus = null; | ||||
if ( ! noEvents ) { | ||||
this._updateFixedColumns(cell.index().column); | ||||
this._emitEvent( 'key-blur', [ this.s.dt, cell ] ); | ||||
} | ||||
}, | ||||
/** | ||||
* Clipboard interaction handlers | ||||
* | ||||
* @private | ||||
*/ | ||||
_clipboard: function () { | ||||
var dt = this.s.dt; | ||||
var that = this; | ||||
var namespace = this.s.namespace; | ||||
// IE8 doesn't support getting selected text | ||||
if ( ! window.getSelection ) { | ||||
return; | ||||
} | ||||
$(document).on( 'copy'+namespace, function (ejq) { | ||||
var e = ejq.originalEvent; | ||||
var selection = window.getSelection().toString(); | ||||
var focused = that.s.lastFocus; | ||||
// Only copy cell text to clipboard if there is no other selection | ||||
// and there is a focused cell | ||||
if ( ! selection && focused ) { | ||||
e.clipboardData.setData( | ||||
'text/plain', | ||||
focused.cell.render( that.c.clipboardOrthogonal ) | ||||
); | ||||
e.preventDefault(); | ||||
} | ||||
} ); | ||||
$(document).on( 'paste'+namespace, function (ejq) { | ||||
var e = ejq.originalEvent; | ||||
var focused = that.s.lastFocus; | ||||
var activeEl = document.activeElement; | ||||
var editor = that.c.editor; | ||||
var pastedText; | ||||
if ( focused && (! activeEl || activeEl.nodeName.toLowerCase() === 'body') ) { | ||||
e.preventDefault(); | ||||
if ( window.clipboardData && window.clipboardData.getData ) { | ||||
// IE | ||||
pastedText = window.clipboardData.getData('Text'); | ||||
} | ||||
else if ( e.clipboardData && e.clipboardData.getData ) { | ||||
// Everything else | ||||
pastedText = e.clipboardData.getData('text/plain'); | ||||
} | ||||
if ( editor ) { | ||||
// Got Editor - need to activate inline editing, | ||||
// set the value and submit | ||||
var options = that._inlineOptions(focused.cell.index()); | ||||
editor | ||||
.inline(options.cell, options.field, options.options) | ||||
.set( editor.displayed()[0], pastedText ) | ||||
.submit(); | ||||
} | ||||
else { | ||||
// No editor, so just dump the data in | ||||
focused.cell.data( pastedText ); | ||||
dt.draw(false); | ||||
} | ||||
} | ||||
} ); | ||||
}, | ||||
/** | ||||
* Get an array of the column indexes that KeyTable can operate on. This | ||||
* is a merge of the user supplied columns and the visible columns. | ||||
* | ||||
* @private | ||||
*/ | ||||
_columns: function () | ||||
{ | ||||
var dt = this.s.dt; | ||||
var user = dt.columns( this.c.columns ).indexes(); | ||||
var out = []; | ||||
dt.columns( ':visible' ).every( function (i) { | ||||
if ( user.indexOf( i ) !== -1 ) { | ||||
out.push( i ); | ||||
} | ||||
} ); | ||||
return out; | ||||
}, | ||||
/** | ||||
* Perform excel like navigation for Editor by triggering an edit on key | ||||
* press | ||||
* | ||||
* @param {integer} key Key code for the pressed key | ||||
* @param {object} orig Original event | ||||
* @private | ||||
*/ | ||||
_editor: function ( key, orig, hardEdit ) | ||||
{ | ||||
// If nothing focused, we can't take any action | ||||
if (! this.s.lastFocus) { | ||||
return; | ||||
} | ||||
// DataTables draw event | ||||
if (orig && orig.type === 'draw') { | ||||
return; | ||||
} | ||||
var that = this; | ||||
var dt = this.s.dt; | ||||
var editor = this.c.editor; | ||||
var editCell = this.s.lastFocus.cell; | ||||
var namespace = this.s.namespace + 'e' + editorNamespaceCounter++; | ||||
// Do nothing if there is already an inline edit in this cell | ||||
if ( $('div.DTE', editCell.node()).length ) { | ||||
return; | ||||
} | ||||
// Don't activate Editor on control key presses | ||||
if ( key !== null && ( | ||||
(key >= 0x00 && key <= 0x09) || | ||||
key === 0x0b || | ||||
key === 0x0c || | ||||
(key >= 0x0e && key <= 0x1f) || | ||||
(key >= 0x70 && key <= 0x7b) || | ||||
(key >= 0x7f && key <= 0x9f) | ||||
) ) { | ||||
return; | ||||
} | ||||
if ( orig ) { | ||||
orig.stopPropagation(); | ||||
// Return key should do nothing - for textareas it would empty the | ||||
// contents | ||||
if ( key === 13 ) { | ||||
orig.preventDefault(); | ||||
} | ||||
} | ||||
var editInline = function () { | ||||
var options = that._inlineOptions(editCell.index()); | ||||
editor | ||||
.one( 'open'+namespace, function () { | ||||
// Remove cancel open | ||||
editor.off( 'cancelOpen'+namespace ); | ||||
// Excel style - select all text | ||||
if ( ! hardEdit ) { | ||||
$('div.DTE_Field_InputControl input, div.DTE_Field_InputControl textarea').select(); | ||||
} | ||||
// Reduce the keys the Keys listens for | ||||
dt.keys.enable( hardEdit ? 'tab-only' : 'navigation-only' ); | ||||
// On blur of the navigation submit | ||||
dt.on( 'key-blur.editor', function (e, dt, cell) { | ||||
if ( editor.displayed() && cell.node() === editCell.node() ) { | ||||
editor.submit(); | ||||
} | ||||
} ); | ||||
// Highlight the cell a different colour on full edit | ||||
if ( hardEdit ) { | ||||
$( dt.table().container() ).addClass('dtk-focus-alt'); | ||||
} | ||||
// If the dev cancels the submit, we need to return focus | ||||
editor.on( 'preSubmitCancelled'+namespace, function () { | ||||
setTimeout( function () { | ||||
that._focus( editCell, null, false ); | ||||
}, 50 ); | ||||
} ); | ||||
editor.on( 'submitUnsuccessful'+namespace, function () { | ||||
that._focus( editCell, null, false ); | ||||
} ); | ||||
// Restore full key navigation on close | ||||
editor.one( 'close'+namespace, function () { | ||||
dt.keys.enable( true ); | ||||
dt.off( 'key-blur.editor' ); | ||||
editor.off( namespace ); | ||||
$( dt.table().container() ).removeClass('dtk-focus-alt'); | ||||
if (that.s.returnSubmit) { | ||||
that.s.returnSubmit = false; | ||||
that._emitEvent( 'key-return-submit', [dt, editCell] ); | ||||
} | ||||
} ); | ||||
} ) | ||||
.one( 'cancelOpen'+namespace, function () { | ||||
// `preOpen` can cancel the display of the form, so it | ||||
// might be that the open event handler isn't needed | ||||
editor.off( namespace ); | ||||
} ) | ||||
.inline(options.cell, options.field, options.options); | ||||
}; | ||||
// Editor 1.7 listens for `return` on keyup, so if return is the trigger | ||||
// key, we need to wait for `keyup` otherwise Editor would just submit | ||||
// the content triggered by this keypress. | ||||
if ( key === 13 ) { | ||||
hardEdit = true; | ||||
$(document).one( 'keyup', function () { // immediately removed | ||||
editInline(); | ||||
} ); | ||||
} | ||||
else { | ||||
editInline(); | ||||
} | ||||
}, | ||||
_inlineOptions: function (cellIdx) | ||||
{ | ||||
if (this.c.editorOptions) { | ||||
return this.c.editorOptions(cellIdx); | ||||
} | ||||
return { | ||||
cell: cellIdx, | ||||
field: undefined, | ||||
options: undefined | ||||
}; | ||||
}, | ||||
/** | ||||
* Emit an event on the DataTable for listeners | ||||
* | ||||
* @param {string} name Event name | ||||
* @param {array} args Event arguments | ||||
* @private | ||||
*/ | ||||
_emitEvent: function ( name, args ) | ||||
{ | ||||
this.s.dt.iterator( 'table', function ( ctx, i ) { | ||||
$(ctx.nTable).triggerHandler( name, args ); | ||||
} ); | ||||
}, | ||||
/** | ||||
* Focus on a particular cell, shifting the table's paging if required | ||||
* | ||||
* @param {DataTables.Api|integer} row Can be given as an API instance that | ||||
* contains the cell to focus or as an integer. As the latter it is the | ||||
* visible row index (from the whole data set) - NOT the data index | ||||
* @param {integer} [column] Not required if a cell is given as the first | ||||
* parameter. Otherwise this is the column data index for the cell to | ||||
* focus on | ||||
* @param {boolean} [shift=true] Should the viewport be moved to show cell | ||||
* @private | ||||
*/ | ||||
_focus: function ( row, column, shift, originalEvent ) | ||||
{ | ||||
var that = this; | ||||
var dt = this.s.dt; | ||||
var pageInfo = dt.page.info(); | ||||
var lastFocus = this.s.lastFocus; | ||||
if ( ! originalEvent) { | ||||
originalEvent = null; | ||||
} | ||||
if ( ! this.s.enable ) { | ||||
return; | ||||
} | ||||
if ( typeof row !== 'number' ) { | ||||
// Its an API instance - check that there is actually a row | ||||
if ( ! row.any() ) { | ||||
return; | ||||
} | ||||
// Convert the cell to a row and column | ||||
var index = row.index(); | ||||
column = index.column; | ||||
row = dt | ||||
.rows( { filter: 'applied', order: 'applied' } ) | ||||
.indexes() | ||||
.indexOf( index.row ); | ||||
// Don't focus rows that were filtered out. | ||||
if ( row < 0 ) { | ||||
return; | ||||
} | ||||
// For server-side processing normalise the row by adding the start | ||||
// point, since `rows().indexes()` includes only rows that are | ||||
// available at the client-side | ||||
if ( pageInfo.serverSide ) { | ||||
row += pageInfo.start; | ||||
} | ||||
} | ||||
// Is the row on the current page? If not, we need to redraw to show the | ||||
// page | ||||
if ( pageInfo.length !== -1 && (row < pageInfo.start || row >= pageInfo.start+pageInfo.length) ) { | ||||
this.s.focusDraw = true; | ||||
this.s.waitingForDraw = true; | ||||
dt | ||||
.one( 'draw', function () { | ||||
that.s.focusDraw = false; | ||||
that.s.waitingForDraw = false; | ||||
that._focus( row, column, undefined, originalEvent ); | ||||
} ) | ||||
.page( Math.floor( row / pageInfo.length ) ) | ||||
.draw( false ); | ||||
return; | ||||
} | ||||
// In the available columns? | ||||
if ( $.inArray( column, this._columns() ) === -1 ) { | ||||
return; | ||||
} | ||||
// De-normalise the server-side processing row, so we select the row | ||||
// in its displayed position | ||||
if ( pageInfo.serverSide ) { | ||||
row -= pageInfo.start; | ||||
} | ||||
// Get the cell from the current position - ignoring any cells which might | ||||
// not have been rendered (therefore can't use `:eq()` selector). | ||||
var cells = dt.cells( null, column, {search: 'applied', order: 'applied'} ).flatten(); | ||||
var cell = dt.cell( cells[ row ] ); | ||||
if ( lastFocus ) { | ||||
// Don't trigger a refocus on the same cell | ||||
if ( lastFocus.node === cell.node() ) { | ||||
this._emitEvent( 'key-refocus', [ this.s.dt, cell, originalEvent || null ] ); | ||||
return; | ||||
} | ||||
// Otherwise blur the old focus | ||||
this._blur(); | ||||
} | ||||
// Clear focus from other tables | ||||
this._removeOtherFocus(); | ||||
var node = $( cell.node() ); | ||||
node.addClass( this.c.className ); | ||||
this._updateFixedColumns(column); | ||||
// Shift viewpoint and page to make cell visible | ||||
if ( shift === undefined || shift === true ) { | ||||
this._scroll( $(window), $(document.body), node, 'offset' ); | ||||
var bodyParent = dt.table().body().parentNode; | ||||
if ( bodyParent !== dt.table().header().parentNode ) { | ||||
var parent = $(bodyParent.parentNode); | ||||
this._scroll( parent, parent, node, 'position' ); | ||||
} | ||||
} | ||||
// Event and finish | ||||
this.s.lastFocus = { | ||||
cell: cell, | ||||
node: cell.node(), | ||||
relative: { | ||||
row: dt.rows( { page: 'current' } ).indexes().indexOf( cell.index().row ), | ||||
column: cell.index().column | ||||
} | ||||
}; | ||||
this._emitEvent( 'key-focus', [ this.s.dt, cell, originalEvent || null ] ); | ||||
dt.state.save(); | ||||
}, | ||||
/** | ||||
* Handle key press | ||||
* | ||||
* @param {object} e Event | ||||
* @private | ||||
*/ | ||||
_key: function ( e ) | ||||
{ | ||||
// If we are waiting for a draw to happen from another key event, then | ||||
// do nothing for this new key press. | ||||
if ( this.s.waitingForDraw ) { | ||||
e.preventDefault(); | ||||
return; | ||||
} | ||||
var enable = this.s.enable; | ||||
this.s.returnSubmit = (enable === 'navigation-only' || enable === 'tab-only') && e.keyCode === 13 | ||||
? true | ||||
: false; | ||||
var navEnable = enable === true || enable === 'navigation-only'; | ||||
if ( ! enable ) { | ||||
return; | ||||
} | ||||
if ( (e.keyCode === 0 || e.ctrlKey || e.metaKey || e.altKey) && !(e.ctrlKey && e.altKey) ) { | ||||
return; | ||||
} | ||||
// If not focused, then there is no key action to take | ||||
var lastFocus = this.s.lastFocus; | ||||
if ( ! lastFocus ) { | ||||
return; | ||||
} | ||||
// And the last focus still exists! | ||||
if ( ! this.s.dt.cell(lastFocus.node).any() ) { | ||||
this.s.lastFocus = null; | ||||
return; | ||||
} | ||||
var that = this; | ||||
var dt = this.s.dt; | ||||
var scrolling = this.s.dt.settings()[0].oScroll.sY ? true : false; | ||||
// If we are not listening for this key, do nothing | ||||
if ( this.c.keys && $.inArray( e.keyCode, this.c.keys ) === -1 ) { | ||||
return; | ||||
} | ||||
switch( e.keyCode ) { | ||||
case 9: // tab | ||||
// `enable` can be tab-only | ||||
this._shift( e, e.shiftKey ? 'left' : 'right', true ); | ||||
break; | ||||
case 27: // esc | ||||
if ( this.c.blurable && enable === true ) { | ||||
this._blur(); | ||||
} | ||||
break; | ||||
case 33: // page up (previous page) | ||||
case 34: // page down (next page) | ||||
if ( navEnable && !scrolling ) { | ||||
e.preventDefault(); | ||||
dt | ||||
.page( e.keyCode === 33 ? 'previous' : 'next' ) | ||||
.draw( false ); | ||||
} | ||||
break; | ||||
case 35: // end (end of current page) | ||||
case 36: // home (start of current page) | ||||
if ( navEnable ) { | ||||
e.preventDefault(); | ||||
var indexes = dt.cells( {page: 'current'} ).indexes(); | ||||
var colIndexes = this._columns(); | ||||
this._focus( dt.cell( | ||||
indexes[ e.keyCode === 35 ? indexes.length-1 : colIndexes[0] ] | ||||
), null, true, e ); | ||||
} | ||||
break; | ||||
case 37: // left arrow | ||||
if ( navEnable ) { | ||||
this._shift( e, 'left' ); | ||||
} | ||||
break; | ||||
case 38: // up arrow | ||||
if ( navEnable ) { | ||||
this._shift( e, 'up' ); | ||||
} | ||||
break; | ||||
case 39: // right arrow | ||||
if ( navEnable ) { | ||||
this._shift( e, 'right' ); | ||||
} | ||||
break; | ||||
case 40: // down arrow | ||||
if ( navEnable ) { | ||||
this._shift( e, 'down' ); | ||||
} | ||||
break; | ||||
case 113: // F2 - Excel like hard edit | ||||
if ( this.c.editor ) { | ||||
this._editor(null, e, true); | ||||
break; | ||||
} | ||||
// else fallthrough | ||||
default: | ||||
// Everything else - pass through only when fully enabled | ||||
if ( enable === true ) { | ||||
this._emitEvent( 'key', [ dt, e.keyCode, this.s.lastFocus.cell, e ] ); | ||||
} | ||||
break; | ||||
} | ||||
}, | ||||
/** | ||||
* Remove focus from all tables other than this one | ||||
*/ | ||||
_removeOtherFocus: function () | ||||
{ | ||||
var thisTable = this.s.dt.table().node(); | ||||
$.fn.dataTable.tables({api:true}).iterator('table', function (settings) { | ||||
if (this.table().node() !== thisTable) { | ||||
this.cell.blur(); | ||||
} | ||||
}); | ||||
}, | ||||
/** | ||||
* Scroll a container to make a cell visible in it. This can be used for | ||||
* both DataTables scrolling and native window scrolling. | ||||
* | ||||
* @param {jQuery} container Scrolling container | ||||
* @param {jQuery} scroller Item being scrolled | ||||
* @param {jQuery} cell Cell in the scroller | ||||
* @param {string} posOff `position` or `offset` - which to use for the | ||||
* calculation. `offset` for the document, otherwise `position` | ||||
* @private | ||||
*/ | ||||
_scroll: function ( container, scroller, cell, posOff ) | ||||
{ | ||||
var offset = cell[posOff](); | ||||
var height = cell.outerHeight(); | ||||
var width = cell.outerWidth(); | ||||
var scrollTop = scroller.scrollTop(); | ||||
var scrollLeft = scroller.scrollLeft(); | ||||
var containerHeight = container.height(); | ||||
var containerWidth = container.width(); | ||||
// If Scroller is being used, the table can be `position: absolute` and that | ||||
// needs to be taken account of in the offset. If no Scroller, this will be 0 | ||||
if ( posOff === 'position' ) { | ||||
offset.top += parseInt( cell.closest('table').css('top'), 10 ); | ||||
} | ||||
// Top correction | ||||
if ( offset.top < scrollTop ) { | ||||
scroller.scrollTop( offset.top ); | ||||
} | ||||
// Left correction | ||||
if ( offset.left < scrollLeft ) { | ||||
scroller.scrollLeft( offset.left ); | ||||
} | ||||
// Bottom correction | ||||
if ( offset.top + height > scrollTop + containerHeight && height < containerHeight ) { | ||||
scroller.scrollTop( offset.top + height - containerHeight ); | ||||
} | ||||
// Right correction | ||||
if ( offset.left + width > scrollLeft + containerWidth && width < containerWidth ) { | ||||
scroller.scrollLeft( offset.left + width - containerWidth ); | ||||
} | ||||
}, | ||||
/** | ||||
* Calculate a single offset movement in the table - up, down, left and | ||||
* right and then perform the focus if possible | ||||
* | ||||
* @param {object} e Event object | ||||
* @param {string} direction Movement direction | ||||
* @param {boolean} keyBlurable `true` if the key press can result in the | ||||
* table being blurred. This is so arrow keys won't blur the table, but | ||||
* tab will. | ||||
* @private | ||||
*/ | ||||
_shift: function ( e, direction, keyBlurable ) | ||||
{ | ||||
var that = this; | ||||
var dt = this.s.dt; | ||||
var pageInfo = dt.page.info(); | ||||
var rows = pageInfo.recordsDisplay; | ||||
var columns = this._columns(); | ||||
var last = this.s.lastFocus; | ||||
if ( ! last ) { | ||||
return; | ||||
} | ||||
var currentCell = last.cell; | ||||
if ( ! currentCell ) { | ||||
return; | ||||
} | ||||
var currRow = dt | ||||
.rows( { filter: 'applied', order: 'applied' } ) | ||||
.indexes() | ||||
.indexOf( currentCell.index().row ); | ||||
// When server-side processing, `rows().indexes()` only gives the rows | ||||
// that are available at the client-side, so we need to normalise the | ||||
// row's current position by the display start point | ||||
if ( pageInfo.serverSide ) { | ||||
currRow += pageInfo.start; | ||||
} | ||||
var currCol = dt | ||||
.columns( columns ) | ||||
.indexes() | ||||
.indexOf( currentCell.index().column ); | ||||
var | ||||
row = currRow, | ||||
column = columns[ currCol ]; // row is the display, column is an index | ||||
// If the direction is rtl then the logic needs to be inverted from this point forwards | ||||
if($(dt.table().node()).css('direction') === 'rtl') { | ||||
if(direction === 'right') { | ||||
direction = 'left'; | ||||
} | ||||
else if(direction === 'left'){ | ||||
direction = 'right'; | ||||
} | ||||
} | ||||
if ( direction === 'right' ) { | ||||
if ( currCol >= columns.length - 1 ) { | ||||
row++; | ||||
column = columns[0]; | ||||
} | ||||
else { | ||||
column = columns[ currCol+1 ]; | ||||
} | ||||
} | ||||
else if ( direction === 'left' ) { | ||||
if ( currCol === 0 ) { | ||||
row--; | ||||
column = columns[ columns.length - 1 ]; | ||||
} | ||||
else { | ||||
column = columns[ currCol-1 ]; | ||||
} | ||||
} | ||||
else if ( direction === 'up' ) { | ||||
row--; | ||||
} | ||||
else if ( direction === 'down' ) { | ||||
row++; | ||||
} | ||||
if ( row >= 0 && row < rows && $.inArray( column, columns ) !== -1 ) { | ||||
if (e) { | ||||
e.preventDefault(); | ||||
} | ||||
this._focus( row, column, true, e ); | ||||
} | ||||
else if ( ! keyBlurable || ! this.c.blurable ) { | ||||
// No new focus, but if the table isn't blurable, then don't loose | ||||
// focus | ||||
if (e) { | ||||
e.preventDefault(); | ||||
} | ||||
} | ||||
else { | ||||
this._blur(); | ||||
} | ||||
}, | ||||
/** | ||||
* Create and insert a hidden input element that can receive focus on behalf | ||||
* of the table | ||||
* | ||||
* @private | ||||
*/ | ||||
_tabInput: function () | ||||
{ | ||||
var that = this; | ||||
var dt = this.s.dt; | ||||
var tabIndex = this.c.tabIndex !== null ? | ||||
this.c.tabIndex : | ||||
dt.settings()[0].iTabIndex; | ||||
if ( tabIndex == -1 ) { | ||||
return; | ||||
} | ||||
// Only create the input element once on first class | ||||
if (! this.s.tabInput) { | ||||
var div = $('<div><input type="text" tabindex="'+tabIndex+'"/></div>') | ||||
.css( { | ||||
position: 'absolute', | ||||
height: 1, | ||||
width: 0, | ||||
overflow: 'hidden' | ||||
} ); | ||||
div.children().on( 'focus', function (e) { | ||||
var cell = dt.cell(':eq(0)', that._columns(), {page: 'current'}); | ||||
if ( cell.any() ) { | ||||
that._focus( cell, null, true, e ); | ||||
} | ||||
} ); | ||||
this.s.tabInput = div; | ||||
} | ||||
// Insert the input element into the first cell in the table's body | ||||
var cell = this.s.dt.cell(':eq(0)', '0:visible', {page: 'current', order: 'current'}).node(); | ||||
if (cell) { | ||||
$(cell).prepend(this.s.tabInput); | ||||
} | ||||
}, | ||||
/** | ||||
* Update fixed columns if they are enabled and if the cell we are | ||||
* focusing is inside a fixed column | ||||
* @param {integer} column Index of the column being changed | ||||
* @private | ||||
*/ | ||||
_updateFixedColumns: function( column ) | ||||
{ | ||||
var dt = this.s.dt; | ||||
var settings = dt.settings()[0]; | ||||
if ( settings._oFixedColumns ) { | ||||
var leftCols = settings._oFixedColumns.s.iLeftColumns; | ||||
var rightCols = settings.aoColumns.length - settings._oFixedColumns.s.iRightColumns; | ||||
if (column < leftCols || column >= rightCols) { | ||||
dt.fixedColumns().update(); | ||||
} | ||||
} | ||||
} | ||||
} ); | ||||
/** | ||||
* KeyTable default settings for initialisation | ||||
* | ||||
* @namespace | ||||
* @name KeyTable.defaults | ||||
* @static | ||||
*/ | ||||
KeyTable.defaults = { | ||||
/** | ||||
* Can focus be removed from the table | ||||
* @type {Boolean} | ||||
*/ | ||||
blurable: true, | ||||
/** | ||||
* Class to give to the focused cell | ||||
* @type {String} | ||||
*/ | ||||
className: 'focus', | ||||
/** | ||||
* Enable or disable clipboard support | ||||
* @type {Boolean} | ||||
*/ | ||||
clipboard: true, | ||||
/** | ||||
* Orthogonal data that should be copied to clipboard | ||||
* @type {string} | ||||
*/ | ||||
clipboardOrthogonal: 'display', | ||||
/** | ||||
* Columns that can be focused. This is automatically merged with the | ||||
* visible columns as only visible columns can gain focus. | ||||
* @type {String} | ||||
*/ | ||||
columns: '', // all | ||||
/** | ||||
* Editor instance to automatically perform Excel like navigation | ||||
* @type {Editor} | ||||
*/ | ||||
editor: null, | ||||
/** | ||||
* Trigger editing immediately on focus | ||||
* @type {boolean} | ||||
*/ | ||||
editOnFocus: false, | ||||
/** | ||||
* Options to pass to Editor's inline method | ||||
* @type {function} | ||||
*/ | ||||
editorOptions: null, | ||||
/** | ||||
* Select a cell to automatically select on start up. `null` for no | ||||
* automatic selection | ||||
* @type {cell-selector} | ||||
*/ | ||||
focus: null, | ||||
/** | ||||
* Array of keys to listen for | ||||
* @type {null|array} | ||||
*/ | ||||
keys: null, | ||||
/** | ||||
* Tab index for where the table should sit in the document's tab flow | ||||
* @type {integer|null} | ||||
*/ | ||||
tabIndex: null | ||||
}; | ||||
KeyTable.version = "2.7.0"; | ||||
$.fn.dataTable.KeyTable = KeyTable; | ||||
$.fn.DataTable.KeyTable = KeyTable; | ||||
DataTable.Api.register( 'cell.blur()', function () { | ||||
return this.iterator( 'table', function (ctx) { | ||||
if ( ctx.keytable ) { | ||||
ctx.keytable.blur(); | ||||
} | ||||
} ); | ||||
} ); | ||||
DataTable.Api.register( 'cell().focus()', function () { | ||||
return this.iterator( 'cell', function (ctx, row, column) { | ||||
if ( ctx.keytable ) { | ||||
ctx.keytable.focus( row, column ); | ||||
} | ||||
} ); | ||||
} ); | ||||
DataTable.Api.register( 'keys.disable()', function () { | ||||
return this.iterator( 'table', function (ctx) { | ||||
if ( ctx.keytable ) { | ||||
ctx.keytable.enable( false ); | ||||
} | ||||
} ); | ||||
} ); | ||||
DataTable.Api.register( 'keys.enable()', function ( opts ) { | ||||
return this.iterator( 'table', function (ctx) { | ||||
if ( ctx.keytable ) { | ||||
ctx.keytable.enable( opts === undefined ? true : opts ); | ||||
} | ||||
} ); | ||||
} ); | ||||
DataTable.Api.register( 'keys.enabled()', function ( opts ) { | ||||
var ctx = this.context; | ||||
if (ctx.length) { | ||||
return ctx[0].keytable | ||||
? ctx[0].keytable.enabled() | ||||
: false; | ||||
} | ||||
return false; | ||||
} ); | ||||
DataTable.Api.register( 'keys.move()', function ( dir ) { | ||||
return this.iterator( 'table', function (ctx) { | ||||
if ( ctx.keytable ) { | ||||
ctx.keytable._shift( null, dir, false ); | ||||
} | ||||
} ); | ||||
} ); | ||||
// Cell selector | ||||
DataTable.ext.selector.cell.push( function ( settings, opts, cells ) { | ||||
var focused = opts.focused; | ||||
var kt = settings.keytable; | ||||
var out = []; | ||||
if ( ! kt || focused === undefined ) { | ||||
return cells; | ||||
} | ||||
for ( var i=0, ien=cells.length ; i<ien ; i++ ) { | ||||
if ( (focused === true && kt.focused( cells[i] ) ) || | ||||
(focused === false && ! kt.focused( cells[i] ) ) | ||||
) { | ||||
out.push( cells[i] ); | ||||
} | ||||
} | ||||
return out; | ||||
} ); | ||||
// Attach a listener to the document which listens for DataTables initialisation | ||||
// events so we can automatically initialise | ||||
$(document).on( 'preInit.dt.dtk', function (e, settings, json) { | ||||
if ( e.namespace !== 'dt' ) { | ||||
return; | ||||
} | ||||
var init = settings.oInit.keys; | ||||
var defaults = DataTable.defaults.keys; | ||||
if ( init || defaults ) { | ||||
var opts = $.extend( {}, defaults, init ); | ||||
if ( init !== false ) { | ||||
new KeyTable( settings, opts ); | ||||
} | ||||
} | ||||
} ); | ||||
return KeyTable; | ||||
})); | ||||