1274 lines
30 KiB
JavaScript
Vendored
1274 lines
30 KiB
JavaScript
Vendored
/*! KeyTable 2.5.3
|
|
* ©2009-2020 SpryMedia Ltd - datatables.net/license
|
|
*/
|
|
|
|
/**
|
|
* @summary KeyTable
|
|
* @description Spreadsheet like keyboard navigation for DataTables
|
|
* @version 2.5.3
|
|
* @file dataTables.keyTable.js
|
|
* @author SpryMedia Ltd (www.sprymedia.co.uk)
|
|
* @contact www.sprymedia.co.uk/contact
|
|
* @copyright Copyright 2009-2020 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;
|
|
},
|
|
|
|
/**
|
|
* 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 ) {
|
|
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();
|
|
} );
|
|
|
|
// 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
|
|
editor
|
|
.inline( focused.cell.index() )
|
|
.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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
orig.stopPropagation();
|
|
|
|
// Return key should do nothing - for textareas it would empty the
|
|
// contents
|
|
if ( key === 13 ) {
|
|
orig.preventDefault();
|
|
}
|
|
|
|
var editInline = function () {
|
|
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');
|
|
} );
|
|
} )
|
|
.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( editCell.index() );
|
|
};
|
|
|
|
// 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();
|
|
}
|
|
},
|
|
|
|
|
|
/**
|
|
* 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;
|
|
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.s.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 currentCell = this.s.lastFocus.cell;
|
|
var columns = this._columns();
|
|
|
|
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 ( 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,
|
|
|
|
/**
|
|
* 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.5.3";
|
|
|
|
|
|
$.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.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;
|
|
}));
|