Subversion-Projekte lars-tiefland.cienc

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
9 lars 1
/*! KeyTable 2.0.0
2
 * ©2009-2015 SpryMedia Ltd - datatables.net/license
3
 */
4
 
5
/**
6
 * @summary     KeyTable
7
 * @description Spreadsheet like keyboard navigation for DataTables
8
 * @version     2.0.0
9
 * @file        dataTables.keyTable.js
10
 * @author      SpryMedia Ltd (www.sprymedia.co.uk)
11
 * @contact     www.sprymedia.co.uk/contact
12
 * @copyright   Copyright 2009-2015 SpryMedia Ltd.
13
 *
14
 * This source file is free software, available under the following license:
15
 *   MIT license - http://datatables.net/license/mit
16
 *
17
 * This source file is distributed in the hope that it will be useful, but
18
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
19
 * or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details.
20
 *
21
 * For details please refer to: http://www.datatables.net
22
 */
23
 
24
 
25
(function(window, document, undefined) {
26
 
27
 
28
var factory = function( $, DataTable ) {
29
"use strict";
30
 
31
 
32
 
33
var KeyTable = function ( dt, opts ) {
34
	// Sanity check that we are using DataTables 1.10 or newer
35
	if ( ! DataTable.versionCheck || ! DataTable.versionCheck( '1.10.8' ) ) {
36
		throw 'KeyTable requires DataTables 1.10.8 or newer';
37
	}
38
 
39
	// User and defaults configuration object
40
	this.c = $.extend( true, {},
41
		DataTable.defaults.keyTable,
42
		KeyTable.defaults,
43
		opts
44
	);
45
 
46
	// Internal settings
47
	this.s = {
48
		/** @type {DataTable.Api} DataTables' API instance */
49
		dt: new DataTable.Api( dt ),
50
 
51
		enable: true
52
	};
53
 
54
	// DOM items
55
	this.dom = {
56
 
57
	};
58
 
59
	// Check if row reorder has already been initialised on this table
60
	var settings = this.s.dt.settings()[0];
61
	var exisiting = settings.keytable;
62
	if ( exisiting ) {
63
		return exisiting;
64
	}
65
 
66
	settings.keytable = this;
67
	this._constructor();
68
};
69
 
70
 
71
KeyTable.prototype = {
72
	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
73
	 * API methods for DataTables API interface
74
	 */
75
 
76
	/**
77
	 * Blur the table's cell focus
78
	 */
79
	blur: function ()
80
	{
81
		this._blur();
82
	},
83
 
84
	/**
85
	 * Enable cell focus for the table
86
	 *
87
	 * @param  {string} state Can be `true`, `false` or `-string navigation-only`
88
	 */
89
	enable: function ( state )
90
	{
91
		this.s.enable = state;
92
	},
93
 
94
	/**
95
	 * Focus on a cell
96
	 * @param  {integer} row    Row index
97
	 * @param  {integer} column Column index
98
	 */
99
	focus: function ( row, column )
100
	{
101
		this._focus( this.s.dt.cell( row, column ) );
102
	},
103
 
104
 
105
	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
106
	 * Constructor
107
	 */
108
 
109
	/**
110
	 * Initialise the KeyTable instance
111
	 *
112
	 * @private
113
	 */
114
	_constructor: function ()
115
	{
116
		this._tabInput();
117
 
118
		var that = this;
119
		var dt = this.s.dt;
120
		var table = $( dt.table().node() );
121
 
122
		// Need to be able to calculate the cell positions relative to the table
123
		if ( table.css('position') === 'static' ) {
124
			table.css( 'position', 'relative' );
125
		}
126
 
127
		// Click to focus
128
		$( dt.table().body() ).on( 'click.keyTable', 'th, td', function () {
129
			if ( that.s.enable === false ) {
130
				return;
131
			}
132
 
133
			var cell = dt.cell( this );
134
 
135
			if ( ! cell.any() ) {
136
				return;
137
			}
138
 
139
			that._focus( cell );
140
		} );
141
 
142
		// Key events
143
		$( document.body ).on( 'keydown.keyTable', function (e) {
144
			that._key( e );
145
		} );
146
 
147
		// Click blur
148
		if ( this.c.blurable ) {
149
			$( document.body ).on( 'click.keyTable', function ( e ) {
150
				// Click on the search input will blur focus
151
				if ( $(e.target).parents( '.dataTables_filter' ).length ) {
152
					that._blur();
153
				}
154
 
155
				// If the click was inside the DataTables container, don't blur
156
				if ( $(e.target).parents().filter( dt.table().container() ).length ) {
157
					return;
158
				}
159
 
160
				// Don't blur in Editor form
161
				if ( $(e.target).parents('div.DTE').length ) {
162
					return;
163
				}
164
 
165
				that._blur();
166
			} );
167
		}
168
 
169
		if ( this.c.editor ) {
170
			dt.on( 'key.kt', function ( e, dt, key, cell, orig ) {
171
				that._editor( key, orig );
172
			} );
173
		}
174
 
175
		// Stave saving
176
		if ( dt.settings()[0].oFeatures.bStateSave ) {
177
			dt.on( 'stateSaveParams.keyTable', function (e, s, d) {
178
				d.keyTable = that.s.lastFocus ?
179
					that.s.lastFocus.index() :
180
					null;
181
			} );
182
		}
183
 
184
		dt.on( 'destroy.keyTable', function () {
185
			dt.off( '.keyTable' );
186
			$( dt.table().body() ).off( 'click.keyTable', 'th, td' );
187
			$( document.body )
188
				.off( 'keydown.keyTable' )
189
				.off( 'click.keyTable' );
190
		} );
191
 
192
		// Initial focus comes from state or options
193
		var state = dt.state.loaded();
194
 
195
		if ( state && state.keyTable ) {
196
			dt.cell( state.keyTable ).focus();
197
		}
198
		else if ( this.c.focus ) {
199
			dt.cell( this.c.focus ).focus();
200
		}
201
	},
202
 
203
 
204
 
205
 
206
	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
207
	 * Private methods
208
	 */
209
 
210
	/**
211
	 * Blur the control
212
	 *
213
	 * @private
214
	 */
215
	_blur: function ()
216
	{
217
		if ( ! this.s.enable || ! this.s.lastFocus ) {
218
			return;
219
		}
220
 
221
		var cell = this.s.lastFocus;
222
 
223
		$( cell.node() ).removeClass( this.c.className );
224
		this.s.lastFocus = null;
225
 
226
		this._emitEvent( 'key-blur', [ this.s.dt, cell ] );
227
	},
228
 
229
 
230
	/**
231
	 * Get an array of the column indexes that KeyTable can operate on. This
232
	 * is a merge of the user supplied columns and the visible columns.
233
	 *
234
	 * @private
235
	 */
236
	_columns: function ()
237
	{
238
		var dt = this.s.dt;
239
		var user = dt.columns( this.c.columns ).indexes();
240
		var out = [];
241
 
242
		dt.columns( ':visible' ).every( function (i) {
243
			if ( user.indexOf( i ) !== -1 ) {
244
				out.push( i );
245
			}
246
		} );
247
 
248
		return out;
249
	},
250
 
251
 
252
	/**
253
	 * Perform excel like navigation for Editor by triggering an edit on key
254
	 * press
255
	 *
256
	 * @param  {integer} key Key code for the pressed key
257
	 * @param  {object} orig Original event
258
	 * @private
259
	 */
260
	_editor: function ( key, orig )
261
	{
262
		var dt = this.s.dt;
263
		var editor = this.c.editor;
264
 
265
		orig.stopPropagation();
266
 
267
		editor.inline( this.s.lastFocus.index() );
268
 
269
		// Excel style - select all text
270
		var input = $('div.DTE input');
271
		if ( input.length ) {
272
			input[0].select();
273
		}
274
 
275
		// Reduce the keys the Keys listens for
276
		dt.keys.enable( 'navigation-only' );
277
 
278
		// On blur of the navigation submit
279
		dt.one( 'key-blur.editor', function () {
280
			if ( editor.displayed() ) {
281
				editor.submit();
282
			}
283
		} );
284
 
285
		// Restore full key navigation on close
286
		editor.one( 'close', function () {
287
			dt.keys.enable( true );
288
			dt.off( 'key-blur.editor' );
289
		} );
290
	},
291
 
292
 
293
	/**
294
	 * Emit an event on the DataTable for listeners
295
	 *
296
	 * @param  {string} name Event name
297
	 * @param  {array} args Event arguments
298
	 * @private
299
	 */
300
	_emitEvent: function ( name, args )
301
	{
302
		this.s.dt.iterator( 'table', function ( ctx, i ) {
303
			$(ctx.nTable).triggerHandler( name, args );
304
		} );
305
	},
306
 
307
 
308
	/**
309
	 * Focus on a particular cell, shifting the table's paging if required
310
	 *
311
	 * @param  {DataTables.Api|integer} row Can be given as an API instance that
312
	 *   contains the cell to focus or as an integer. As the latter it is the
313
	 *   visible row index - NOT the data index
314
	 * @param  {integer} [column] Not required if a cell is given as the first
315
	 *   parameter. Otherwise this is the column data index for the cell to
316
	 *   focus on
317
	 * @private
318
	 */
319
	_focus: function ( row, column )
320
	{
321
		var that = this;
322
		var dt = this.s.dt;
323
		var pageInfo = dt.page.info();
324
		var lastFocus = this.s.lastFocus;
325
 
326
		if ( ! this.s.enable ) {
327
			return;
328
		}
329
 
330
		if ( typeof row !== 'number' ) {
331
			// Convert the cell to a row and column
332
			var index = row.index();
333
			column = index.column;
334
			row = dt
335
				.rows( { filter: 'applied', order: 'applied' } )
336
				.indexes()
337
				.indexOf( index.row );
338
 
339
			// For server-side processing normalise the row by adding the start
340
			// point, since `rows().indexes()` includes only rows that are
341
			// available at the client-side
342
			if ( pageInfo.serverSide ) {
343
				row += pageInfo.start;
344
			}
345
		}
346
 
347
		// Is the row on the current page? If not, we need to redraw to show the
348
		// page
349
		if ( row < pageInfo.start || row >= pageInfo.start+pageInfo.length ) {
350
			dt
351
				.one( 'draw', function () {
352
					that._focus( row, column );
353
				} )
354
				.page( Math.floor( row / pageInfo.length ) )
355
				.draw( false );
356
 
357
			return;
358
		}
359
 
360
		// In the available columns?
361
		if ( $.inArray( column, this._columns() ) === -1 ) {
362
			return;
363
		}
364
 
365
		// De-normalise the server-side processing row, so we select the row
366
		// in its displayed position
367
		if ( pageInfo.serverSide ) {
368
			row -= pageInfo.start;
369
		}
370
 
371
		var cell = dt.cell( ':eq('+row+')', column );
372
 
373
		if ( lastFocus ) {
374
			// Don't trigger a refocus on the same cell
375
			if ( lastFocus.node() === cell.node() ) {
376
				return;
377
			}
378
 
379
			// Otherwise blur the old focus
380
			this._blur();
381
		}
382
 
383
		var node = $( cell.node() );
384
		node.addClass( this.c.className );
385
 
386
		// Shift viewpoint and page to make cell visible
387
		this._scroll( $(window), $(document.body), node, 'offset' );
388
 
389
		var bodyParent = dt.table().body().parentNode;
390
		if ( bodyParent !== dt.table().header().parentNode ) {
391
			var parent = $(bodyParent.parentNode);
392
 
393
			this._scroll( parent, parent, node, 'position' );
394
		}
395
 
396
		// Event and finish
397
		this.s.lastFocus = cell;
398
 
399
		this._emitEvent( 'key-focus', [ this.s.dt, cell ] );
400
		dt.state.save();
401
	},
402
 
403
 
404
	/**
405
	 * Handle key press
406
	 *
407
	 * @param  {object} e Event
408
	 * @private
409
	 */
410
	_key: function ( e )
411
	{
412
		if ( ! this.s.enable ) {
413
			return;
414
		}
415
 
416
		if ( e.keyCode === 0 || e.ctrlKey || e.metaKey || e.altKey ) {
417
			return;
418
		}
419
 
420
		// If not focused, then there is no key action to take
421
		var cell = this.s.lastFocus;
422
		if ( ! cell ) {
423
			return;
424
		}
425
 
426
		var that = this;
427
		var dt = this.s.dt;
428
 
429
		// If we are not listening for this key, do nothing
430
		if ( this.c.keys && $.inArray( e.keyCode, this.c.keys ) === -1 ) {
431
			return;
432
		}
433
 
434
		switch( e.keyCode ) {
435
			case 9: // tab
436
				this._shift( e, e.shiftKey ? 'left' : 'right', true );
437
				break;
438
 
439
			case 27: // esc
440
				if ( this.s.blurable && this.s.enable === true ) {
441
					this._blur();
442
				}
443
				break;
444
 
445
			case 33: // page up (previous page)
446
			case 34: // page down (next page)
447
				e.preventDefault();
448
				var index = dt.cells( {page: 'current'} ).nodes().indexOf( cell.node() );
449
 
450
				dt
451
					.one( 'draw', function () {
452
						var nodes = dt.cells( {page: 'current'} ).nodes();
453
 
454
						that._focus( dt.cell( index < nodes.length ?
455
							nodes[ index ] :
456
							nodes[ nodes.length-1 ]
457
						) );
458
					} )
459
					.page( e.keyCode === 33 ? 'previous' : 'next' )
460
					.draw( false );
461
				break;
462
 
463
			case 35: // end (end of current page)
464
			case 36: // home (start of current page)
465
				e.preventDefault();
466
				var indexes = dt.cells( {page: 'current'} ).indexes();
467
 
468
				this._focus( dt.cell(
469
					indexes[ e.keyCode === 35 ? indexes.length-1 : 0 ]
470
				) );
471
				break;
472
 
473
			case 37: // left arrow
474
				this._shift( e, 'left' );
475
				break;
476
 
477
			case 38: // up arrow
478
				this._shift( e, 'up' );
479
				break;
480
 
481
			case 39: // right arrow
482
				this._shift( e, 'right' );
483
				break;
484
 
485
			case 40: // down arrow
486
				this._shift( e, 'down' );
487
				break;
488
 
489
			default:
490
				// Everything else - pass through only when fully enabled
491
				if ( this.s.enable === true ) {
492
					this._emitEvent( 'key', [ dt, e.keyCode, this.s.lastFocus, e ] );
493
				}
494
				break;
495
		}
496
	},
497
 
498
 
499
	/**
500
	 * Scroll a container to make a cell visible in it. This can be used for
501
	 * both DataTables scrolling and native window scrolling.
502
	 *
503
	 * @param  {jQuery} container Scrolling container
504
	 * @param  {jQuery} scroller  Item being scrolled
505
	 * @param  {jQuery} cell      Cell in the scroller
506
	 * @param  {string} posOff    `position` or `offset` - which to use for the
507
	 *   calculation. `offset` for the document, otherwise `position`
508
	 * @private
509
	 */
510
	_scroll: function ( container, scroller, cell, posOff )
511
	{
512
		var offset = cell[posOff]();
513
		var height = cell.outerHeight();
514
		var width = cell.outerWidth();
515
 
516
		var scrollTop = scroller.scrollTop();
517
		var scrollLeft = scroller.scrollLeft();
518
		var containerHeight = container.height();
519
		var containerWidth = container.width();
520
 
521
		// Top correction
522
		if ( offset.top < scrollTop ) {
523
			scroller.scrollTop( offset.top );
524
		}
525
 
526
		// Left correction
527
		if ( offset.left < scrollLeft ) {
528
			scroller.scrollLeft( offset.left );
529
		}
530
 
531
		// Bottom correction
532
		if ( offset.top + height > scrollTop + containerHeight ) {
533
			scroller.scrollTop( offset.top + height - containerHeight );
534
		}
535
 
536
		// Right correction
537
		if ( offset.left + width > scrollLeft + containerWidth ) {
538
			scroller.scrollLeft( offset.left + width - containerWidth );
539
		}
540
	},
541
 
542
 
543
	/**
544
	 * Calculate a single offset movement in the table - up, down, left and
545
	 * right and then perform the focus if possible
546
	 *
547
	 * @param  {object}  e           Event object
548
	 * @param  {string}  direction   Movement direction
549
	 * @param  {boolean} keyBlurable `true` if the key press can result in the
550
	 *   table being blurred. This is so arrow keys won't blur the table, but
551
	 *   tab will.
552
	 * @private
553
	 */
554
	_shift: function ( e, direction, keyBlurable )
555
	{
556
		var that         = this;
557
		var dt           = this.s.dt;
558
		var pageInfo     = dt.page.info();
559
		var rows         = pageInfo.recordsDisplay;
560
		var currentCell  = this.s.lastFocus;
561
		var columns      = this._columns();
562
 
563
		if ( ! currentCell ) {
564
			return;
565
		}
566
 
567
		var currRow = dt
568
			.rows( { filter: 'applied', order: 'applied' } )
569
			.indexes()
570
			.indexOf( currentCell.index().row );
571
 
572
		// When server-side processing, `rows().indexes()` only gives the rows
573
		// that are available at the client-side, so we need to normalise the
574
		// row's current position by the display start point
575
		if ( pageInfo.serverSide ) {
576
			currRow += pageInfo.start;
577
		}
578
 
579
		var currCol = dt
580
			.columns( columns )
581
			.indexes()
582
			.indexOf( currentCell.index().column );
583
 
584
		var
585
			row = currRow,
586
			column = columns[ currCol ]; // row is the display, column is an index
587
 
588
		if ( direction === 'right' ) {
589
			if ( currCol >= columns.length - 1 ) {
590
				row++;
591
				column = columns[0];
592
			}
593
			else {
594
				column = columns[ currCol+1 ];
595
			}
596
		}
597
		else if ( direction === 'left' ) {
598
			if ( currCol === 0 ) {
599
				row--;
600
				column = columns[ columns.length - 1 ];
601
			}
602
			else {
603
				column = columns[ currCol-1 ];
604
			}
605
		}
606
		else if ( direction === 'up' ) {
607
			row--;
608
		}
609
		else if ( direction === 'down' ) {
610
			row++;
611
		}
612
 
613
		if ( row    >= 0 && row    < rows &&
614
			 column >= 0 && column <= columns.length
615
		) {
616
			e.preventDefault();
617
 
618
			this._focus( row, column );
619
		}
620
		else if ( ! keyBlurable || ! this.c.blurable ) {
621
			// No new focus, but if the table isn't blurable, then don't loose
622
			// focus
623
			e.preventDefault();
624
		}
625
		else {
626
			this._blur();
627
		}
628
	},
629
 
630
 
631
	/**
632
	 * Create a hidden input element that can receive focus on behalf of the
633
	 * table
634
	 *
635
	 * @private
636
	 */
637
	_tabInput: function ()
638
	{
639
		var that = this;
640
		var dt = this.s.dt;
641
		var tabIndex = this.c.tabIndex !== null ?
642
			this.c.tabIndex :
643
			dt.settings()[0].iTabIndex;
644
 
645
		if ( tabIndex == -1 ) {
646
			return;
647
		}
648
 
649
		var div = $('<div><input type="text" tabindex="'+tabIndex+'"/></div>')
650
			.css( {
651
				position: 'absolute',
652
				height: 1,
653
				width: 0,
654
				overflow: 'hidden'
655
			} )
656
			.insertBefore( dt.table().node() );
657
 
658
		div.children().on( 'focus', function () {
659
			that._focus( dt.cell(':eq(0)', {page: 'current'}) );
660
		} );
661
	}
662
};
663
 
664
 
665
/**
666
 * KeyTable default settings for initialisation
667
 *
668
 * @namespace
669
 * @name KeyTable.defaults
670
 * @static
671
 */
672
KeyTable.defaults = {
673
	/**
674
	 * Can focus be removed from the table
675
	 * @type {Boolean}
676
	 */
677
	blurable: true,
678
 
679
	/**
680
	 * Class to give to the focused cell
681
	 * @type {String}
682
	 */
683
	className: 'focus',
684
 
685
	/**
686
	 * Columns that can be focused. This is automatically merged with the
687
	 * visible columns as only visible columns can gain focus.
688
	 * @type {String}
689
	 */
690
	columns: '', // all
691
 
692
	/**
693
	 * Editor instance to automatically perform Excel like navigation
694
	 * @type {Editor}
695
	 */
696
	editor: null,
697
 
698
	/**
699
	 * Select a cell to automatically select on start up. `null` for no
700
	 * automatic selection
701
	 * @type {cell-selector}
702
	 */
703
	focus: null,
704
 
705
	/**
706
	 * Array of keys to listen for
707
	 * @type {null|array}
708
	 */
709
	keys: null,
710
 
711
	/**
712
	 * Tab index for where the table should sit in the document's tab flow
713
	 * @type {integer|null}
714
	 */
715
	tabIndex: null
716
};
717
 
718
 
719
 
720
KeyTable.version = "2.0.0";
721
 
722
 
723
$.fn.dataTable.KeyTable = KeyTable;
724
$.fn.DataTable.KeyTable = KeyTable;
725
 
726
 
727
DataTable.Api.register( 'cell.blur()', function () {
728
	return this.iterator( 'table', function (ctx) {
729
		if ( ctx.keytable ) {
730
			ctx.keytable.blur();
731
		}
732
	} );
733
} );
734
 
735
DataTable.Api.register( 'cell().focus()', function () {
736
	return this.iterator( 'cell', function (ctx, row, column) {
737
		if ( ctx.keytable ) {
738
			ctx.keytable.focus( row, column );
739
		}
740
	} );
741
} );
742
 
743
DataTable.Api.register( 'keys.disable()', function () {
744
	return this.iterator( 'table', function (ctx) {
745
		if ( ctx.keytable ) {
746
			ctx.keytable.enable( false );
747
		}
748
	} );
749
} );
750
 
751
DataTable.Api.register( 'keys.enable()', function ( opts ) {
752
	return this.iterator( 'table', function (ctx) {
753
		if ( ctx.keytable ) {
754
			ctx.keytable.enable( opts === undefined ? true : opts );
755
		}
756
	} );
757
} );
758
 
759
 
760
// Attach a listener to the document which listens for DataTables initialisation
761
// events so we can automatically initialise
762
$(document).on( 'preInit.dt.dtk', function (e, settings, json) {
763
	if ( e.namespace !== 'dt' ) {
764
		return;
765
	}
766
 
767
	var init = settings.oInit.keys;
768
	var defaults = DataTable.defaults.keys;
769
 
770
	if ( init || defaults ) {
771
		var opts = $.extend( {}, init, defaults );
772
 
773
		if ( init !== false ) {
774
			new KeyTable( settings, opts  );
775
		}
776
	}
777
} );
778
 
779
 
780
return KeyTable;
781
}; // /factory
782
 
783
 
784
// Define as an AMD module if possible
785
if ( typeof define === 'function' && define.amd ) {
786
	define( ['jquery', 'datatables'], factory );
787
}
788
else if ( typeof exports === 'object' ) {
789
    // Node/CommonJS
790
    factory( require('jquery'), require('datatables') );
791
}
792
else if ( jQuery && !jQuery.fn.dataTable.KeyTable ) {
793
	// Otherwise simply initialise as normal, stopping multiple evaluation
794
	factory( jQuery, jQuery.fn.dataTable );
795
}
796
 
797
 
798
})(window, document);