]>
Commit | Line | Data |
---|---|---|
31f18b77 FG |
1 | /*! KeyTable 1.2.1 |
2 | * ©2010-2014 SpryMedia Ltd - datatables.net/license | |
3 | */ | |
4 | ||
5 | /** | |
6 | * @summary KeyTable | |
7 | * @description Spreadsheet like keyboard navigation for DataTables | |
8 | * @version 1.2.1 | |
9 | * @file dataTables.keyTable.js | |
10 | * @author SpryMedia Ltd (www.sprymedia.co.uk) | |
11 | * @contact www.sprymedia.co.uk/contact | |
12 | * @copyright Copyright 2009-2014 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 | // Global scope for KeyTable for backwards compatibility. Will be removed in 1.3 | |
25 | var KeyTable; | |
26 | ||
27 | ||
28 | (function(window, document, undefined) { | |
29 | ||
30 | ||
31 | var factory = function( $, DataTable ) { | |
32 | "use strict"; | |
33 | ||
34 | KeyTable = function ( oInit ) | |
35 | { | |
36 | /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * | |
37 | * API parameters | |
38 | * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ | |
39 | ||
40 | /* | |
41 | * Variable: block | |
42 | * Purpose: Flag whether or not KeyTable events should be processed | |
43 | * Scope: KeyTable - public | |
44 | */ | |
45 | this.block = false; | |
46 | ||
47 | /* | |
48 | * Variable: event | |
49 | * Purpose: Container for all event application methods | |
50 | * Scope: KeyTable - public | |
51 | * Notes: This object contains all the public methods for adding and removing events - these | |
52 | * are dynamically added later on | |
53 | */ | |
54 | this.event = { | |
55 | "remove": {} | |
56 | }; | |
57 | ||
58 | ||
59 | /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * | |
60 | * API methods | |
61 | * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ | |
62 | ||
63 | /* | |
64 | * Function: fnGetCurrentPosition | |
65 | * Purpose: Get the currently focused cell's position | |
66 | * Returns: array int: [ x, y ] | |
67 | * Inputs: void | |
68 | */ | |
69 | this.fnGetCurrentPosition = function () | |
70 | { | |
71 | return [ _iOldX, _iOldY ]; | |
72 | }; | |
73 | ||
74 | ||
75 | /* | |
76 | * Function: fnGetCurrentData | |
77 | * Purpose: Get the currently focused cell's data (innerHTML) | |
78 | * Returns: string: - data requested | |
79 | * Inputs: void | |
80 | */ | |
81 | this.fnGetCurrentData = function () | |
82 | { | |
83 | return _nOldFocus.innerHTML; | |
84 | }; | |
85 | ||
86 | ||
87 | /* | |
88 | * Function: fnGetCurrentTD | |
89 | * Purpose: Get the currently focused cell | |
90 | * Returns: node: - focused element | |
91 | * Inputs: void | |
92 | */ | |
93 | this.fnGetCurrentTD = function () | |
94 | { | |
95 | return _nOldFocus; | |
96 | }; | |
97 | ||
98 | ||
99 | /* | |
100 | * Function: fnSetPosition | |
101 | * Purpose: Set the position of the focused cell | |
102 | * Returns: - | |
103 | * Inputs: int:x - x coordinate | |
104 | * int:y - y coordinate | |
105 | * Notes: Thanks to Rohan Daxini for the basis of this function | |
106 | */ | |
107 | this.fnSetPosition = function( x, y ) | |
108 | { | |
109 | if ( typeof x == 'object' && x.nodeName ) | |
110 | { | |
111 | _fnSetFocus( x ); | |
112 | } | |
113 | else | |
114 | { | |
115 | _fnSetFocus( _fnCellFromCoords(x, y) ); | |
116 | } | |
117 | }; | |
118 | ||
119 | ||
120 | /* | |
121 | * Function: fnBlur | |
122 | * Purpose: Blur the current focus | |
123 | * Returns: - | |
124 | * Inputs: - | |
125 | */ | |
126 | this.fnBlur = function() | |
127 | { | |
128 | _fnBlur(); | |
129 | }; | |
130 | ||
131 | ||
132 | /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * | |
133 | * Private parameters | |
134 | * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ | |
135 | ||
136 | /* | |
137 | * Variable: _nBody | |
138 | * Purpose: Body node of the table - cached for renference | |
139 | * Scope: KeyTable - private | |
140 | */ | |
141 | var _nBody = null; | |
142 | ||
143 | /* | |
144 | * Variable: | |
145 | * Purpose: | |
146 | * Scope: KeyTable - private | |
147 | */ | |
148 | var _nOldFocus = null; | |
149 | ||
150 | /* | |
151 | * Variable: _iOldX and _iOldY | |
152 | * Purpose: X and Y coords of the old elemet that was focused on | |
153 | * Scope: KeyTable - private | |
154 | */ | |
155 | var _iOldX = null; | |
156 | var _iOldY = null; | |
157 | ||
158 | /* | |
159 | * Variable: _that | |
160 | * Purpose: Scope saving for 'this' after a jQuery event | |
161 | * Scope: KeyTable - private | |
162 | */ | |
163 | var _that = null; | |
164 | ||
165 | /* | |
166 | * Variable: sFocusClass | |
167 | * Purpose: Class that should be used for focusing on a cell | |
168 | * Scope: KeyTable - private | |
169 | */ | |
170 | var _sFocusClass = "focus"; | |
171 | ||
172 | /* | |
173 | * Variable: _bKeyCapture | |
174 | * Purpose: Flag for should KeyTable capture key events or not | |
175 | * Scope: KeyTable - private | |
176 | */ | |
177 | var _bKeyCapture = false; | |
178 | ||
179 | /* | |
180 | * Variable: _oaoEvents | |
181 | * Purpose: Event cache object, one array for each supported event for speed of searching | |
182 | * Scope: KeyTable - private | |
183 | */ | |
184 | var _oaoEvents = { | |
185 | "action": [], | |
186 | "esc": [], | |
187 | "focus": [], | |
188 | "blur": [] | |
189 | }; | |
190 | ||
191 | /* | |
192 | * Variable: _oDatatable | |
193 | * Purpose: DataTables settings object for if we are actually using a | |
194 | * DataTables table | |
195 | * Scope: KeyTable - private | |
196 | */ | |
197 | var _oDatatable = null; | |
198 | ||
199 | var _bForm; | |
200 | var _nInput; | |
201 | var _bInputFocused = false; | |
202 | ||
203 | ||
204 | /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * | |
205 | * Private methods | |
206 | * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ | |
207 | ||
208 | /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * | |
209 | * Key table events | |
210 | */ | |
211 | ||
212 | /* | |
213 | * Function: _fnEventAddTemplate | |
214 | * Purpose: Create a function (with closure for sKey) event addition API | |
215 | * Returns: function: - template function | |
216 | * Inputs: string:sKey - type of event to detect | |
217 | */ | |
218 | function _fnEventAddTemplate( sKey ) | |
219 | { | |
220 | /* | |
221 | * Function: - | |
222 | * Purpose: API function for adding event to cache | |
223 | * Returns: - | |
224 | * Inputs: 1. node:x - target node to add event for | |
225 | * 2. function:y - callback function to apply | |
226 | * or | |
227 | * 1. int:x - x coord. of target cell (can be null for live events) | |
228 | * 2. int:y - y coord. of target cell (can be null for live events) | |
229 | * 3. function:z - callback function to apply | |
230 | * Notes: This function is (interally) overloaded (in as much as javascript allows for | |
231 | * that) - the target cell can be given by either node or coords. | |
232 | */ | |
233 | return function ( x, y, z ) { | |
234 | if ( (x===null || typeof x == "number") && | |
235 | (y===null || typeof y == "number") && | |
236 | typeof z == "function" ) | |
237 | { | |
238 | _fnEventAdd( sKey, x, y, z ); | |
239 | } | |
240 | else if ( typeof x == "object" && typeof y == "function" ) | |
241 | { | |
242 | var aCoords = _fnCoordsFromCell( x ); | |
243 | _fnEventAdd( sKey, aCoords[0], aCoords[1], y ); | |
244 | } | |
245 | else | |
246 | { | |
247 | alert( "Unhandable event type was added: x" +x+ " y:" +y+ " z:" +z ); | |
248 | } | |
249 | }; | |
250 | } | |
251 | ||
252 | ||
253 | /* | |
254 | * Function: _fnEventRemoveTemplate | |
255 | * Purpose: Create a function (with closure for sKey) event removal API | |
256 | * Returns: function: - template function | |
257 | * Inputs: string:sKey - type of event to detect | |
258 | */ | |
259 | function _fnEventRemoveTemplate( sKey ) | |
260 | { | |
261 | /* | |
262 | * Function: - | |
263 | * Purpose: API function for removing event from cache | |
264 | * Returns: int: - number of events removed | |
265 | * Inputs: 1. node:x - target node to remove event from | |
266 | * 2. function:y - callback function to apply | |
267 | * or | |
268 | * 1. int:x - x coord. of target cell (can be null for live events) | |
269 | * 2. int:y - y coord. of target cell (can be null for live events) | |
270 | * 3. function:z - callback function to remove - optional | |
271 | * Notes: This function is (interally) overloaded (in as much as javascript allows for | |
272 | * that) - the target cell can be given by either node or coords and the function | |
273 | * to remove is optional | |
274 | */ | |
275 | return function ( x, y, z ) { | |
276 | if ( (x===null || typeof arguments[0] == "number") && | |
277 | (y===null || typeof arguments[1] == "number" ) ) | |
278 | { | |
279 | if ( typeof arguments[2] == "function" ) | |
280 | { | |
281 | _fnEventRemove( sKey, x, y, z ); | |
282 | } | |
283 | else | |
284 | { | |
285 | _fnEventRemove( sKey, x, y ); | |
286 | } | |
287 | } | |
288 | else if ( typeof arguments[0] == "object" ) | |
289 | { | |
290 | var aCoords = _fnCoordsFromCell( x ); | |
291 | if ( typeof arguments[1] == "function" ) | |
292 | { | |
293 | _fnEventRemove( sKey, aCoords[0], aCoords[1], y ); | |
294 | } | |
295 | else | |
296 | { | |
297 | _fnEventRemove( sKey, aCoords[0], aCoords[1] ); | |
298 | } | |
299 | } | |
300 | else | |
301 | { | |
302 | alert( "Unhandable event type was removed: x" +x+ " y:" +y+ " z:" +z ); | |
303 | } | |
304 | }; | |
305 | } | |
306 | ||
307 | /* Use the template functions to add the event API functions */ | |
308 | for ( var sKey in _oaoEvents ) | |
309 | { | |
310 | if ( sKey ) | |
311 | { | |
312 | this.event[sKey] = _fnEventAddTemplate( sKey ); | |
313 | this.event.remove[sKey] = _fnEventRemoveTemplate( sKey ); | |
314 | } | |
315 | } | |
316 | ||
317 | ||
318 | /* | |
319 | * Function: _fnEventAdd | |
320 | * Purpose: Add an event to the internal cache | |
321 | * Returns: - | |
322 | * Inputs: string:sType - type of event to add, given by the available elements in _oaoEvents | |
323 | * int:x - x-coords to add event to - can be null for "blanket" event | |
324 | * int:y - y-coords to add event to - can be null for "blanket" event | |
325 | * function:fn - callback function for when triggered | |
326 | */ | |
327 | function _fnEventAdd( sType, x, y, fn ) | |
328 | { | |
329 | _oaoEvents[sType].push( { | |
330 | "x": x, | |
331 | "y": y, | |
332 | "fn": fn | |
333 | } ); | |
334 | } | |
335 | ||
336 | ||
337 | /* | |
338 | * Function: _fnEventRemove | |
339 | * Purpose: Remove an event from the event cache | |
340 | * Returns: int: - number of matching events removed | |
341 | * Inputs: string:sType - type of event to look for | |
342 | * node:nTarget - target table cell | |
343 | * function:fn - optional - remove this function. If not given all handlers of this | |
344 | * type will be removed | |
345 | */ | |
346 | function _fnEventRemove( sType, x, y, fn ) | |
347 | { | |
348 | var iCorrector = 0; | |
349 | ||
350 | for ( var i=0, iLen=_oaoEvents[sType].length ; i<iLen-iCorrector ; i++ ) | |
351 | { | |
352 | if ( typeof fn != 'undefined' ) | |
353 | { | |
354 | if ( _oaoEvents[sType][i-iCorrector].x == x && | |
355 | _oaoEvents[sType][i-iCorrector].y == y && | |
356 | _oaoEvents[sType][i-iCorrector].fn == fn ) | |
357 | { | |
358 | _oaoEvents[sType].splice( i-iCorrector, 1 ); | |
359 | iCorrector++; | |
360 | } | |
361 | } | |
362 | else | |
363 | { | |
364 | if ( _oaoEvents[sType][i-iCorrector].x == x && | |
365 | _oaoEvents[sType][i-iCorrector].y == y ) | |
366 | { | |
367 | _oaoEvents[sType].splice( i, 1 ); | |
368 | return 1; | |
369 | } | |
370 | } | |
371 | } | |
372 | return iCorrector; | |
373 | } | |
374 | ||
375 | ||
376 | /* | |
377 | * Function: _fnEventFire | |
378 | * Purpose: Look thought the events cache and fire off the event of interest | |
379 | * Returns: int:iFired - number of events fired | |
380 | * Inputs: string:sType - type of event to look for | |
381 | * int:x - x coord of cell | |
382 | * int:y - y coord of ell | |
383 | * Notes: It might be more efficient to return after the first event has been tirggered, | |
384 | * but that would mean that only one function of a particular type can be | |
385 | * subscribed to a particular node. | |
386 | */ | |
387 | function _fnEventFire ( sType, x, y ) | |
388 | { | |
389 | var iFired = 0; | |
390 | var aEvents = _oaoEvents[sType]; | |
391 | for ( var i=0 ; i<aEvents.length ; i++ ) | |
392 | { | |
393 | if ( (aEvents[i].x == x && aEvents[i].y == y ) || | |
394 | (aEvents[i].x === null && aEvents[i].y == y ) || | |
395 | (aEvents[i].x == x && aEvents[i].y === null ) || | |
396 | (aEvents[i].x === null && aEvents[i].y === null ) | |
397 | ) | |
398 | { | |
399 | aEvents[i].fn( _fnCellFromCoords(x,y), x, y ); | |
400 | iFired++; | |
401 | } | |
402 | } | |
403 | return iFired; | |
404 | } | |
405 | ||
406 | ||
407 | ||
408 | /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * | |
409 | * Focus functions | |
410 | */ | |
411 | ||
412 | /* | |
413 | * Function: _fnSetFocus | |
414 | * Purpose: Set focus on a node, and remove from an old node if needed | |
415 | * Returns: - | |
416 | * Inputs: node:nTarget - node we want to focus on | |
417 | * bool:bAutoScroll - optional - should we scroll the view port to the display | |
418 | */ | |
419 | function _fnSetFocus( nTarget, bAutoScroll ) | |
420 | { | |
421 | /* If node already has focus, just ignore this call */ | |
422 | if ( _nOldFocus == nTarget ) | |
423 | { | |
424 | return; | |
425 | } | |
426 | ||
427 | if ( typeof bAutoScroll == 'undefined' ) | |
428 | { | |
429 | bAutoScroll = true; | |
430 | } | |
431 | ||
432 | /* Remove old focus (with blur event if needed) */ | |
433 | if ( _nOldFocus !== null ) | |
434 | { | |
435 | _fnRemoveFocus( _nOldFocus ); | |
436 | } | |
437 | ||
438 | /* Add the new class to highlight the focused cell */ | |
439 | $(nTarget).addClass( _sFocusClass ); | |
440 | $(nTarget).parent().addClass( _sFocusClass ); | |
441 | ||
442 | /* If it's a DataTable then we need to jump the paging to the relevant page */ | |
443 | var oSettings; | |
444 | if ( _oDatatable ) | |
445 | { | |
446 | oSettings = _oDatatable; | |
447 | var iRow = _fnFindDtCell( nTarget )[1]; | |
448 | var bKeyCaptureCache = _bKeyCapture; | |
449 | ||
450 | /* Page forwards */ | |
451 | while ( iRow >= oSettings.fnDisplayEnd() ) | |
452 | { | |
453 | if ( oSettings._iDisplayLength >= 0 ) | |
454 | { | |
455 | /* Make sure we are not over running the display array */ | |
456 | if ( oSettings._iDisplayStart + oSettings._iDisplayLength < oSettings.fnRecordsDisplay() ) | |
457 | { | |
458 | oSettings._iDisplayStart += oSettings._iDisplayLength; | |
459 | } | |
460 | } | |
461 | else | |
462 | { | |
463 | oSettings._iDisplayStart = 0; | |
464 | } | |
465 | _oDatatable.oApi._fnCalculateEnd( oSettings ); | |
466 | } | |
467 | ||
468 | /* Page backwards */ | |
469 | while ( iRow < oSettings._iDisplayStart ) | |
470 | { | |
471 | oSettings._iDisplayStart = oSettings._iDisplayLength>=0 ? | |
472 | oSettings._iDisplayStart - oSettings._iDisplayLength : | |
473 | 0; | |
474 | ||
475 | if ( oSettings._iDisplayStart < 0 ) | |
476 | { | |
477 | oSettings._iDisplayStart = 0; | |
478 | } | |
479 | _oDatatable.oApi._fnCalculateEnd( oSettings ); | |
480 | } | |
481 | ||
482 | /* Re-draw the table */ | |
483 | _oDatatable.oApi._fnDraw( oSettings ); | |
484 | ||
485 | /* Restore the key capture */ | |
486 | _bKeyCapture = bKeyCaptureCache; | |
487 | } | |
488 | ||
489 | /* Cache the information that we are interested in */ | |
490 | var aNewPos = _fnCoordsFromCell( nTarget ); | |
491 | _nOldFocus = nTarget; | |
492 | _iOldX = aNewPos[0]; | |
493 | _iOldY = aNewPos[1]; | |
494 | ||
495 | var iViewportHeight, iViewportWidth, iScrollTop, iScrollLeft, iHeight, iWidth, aiPos; | |
496 | if ( bAutoScroll ) | |
497 | { | |
498 | /* Scroll the viewport such that the new cell is fully visible in the rendered window */ | |
499 | iViewportHeight = $(window).height(); | |
500 | iViewportWidth = $(window).width(); | |
501 | iScrollTop = $(document).scrollTop(); | |
502 | iScrollLeft = $(document).scrollLeft(); | |
503 | iHeight = nTarget.offsetHeight; | |
504 | iWidth = nTarget.offsetWidth; | |
505 | aiPos = _fnGetPos( nTarget ); | |
506 | ||
507 | /* Take account of scrolling in DataTables 1.7 - remove scrolling since that would add to | |
508 | * the positioning calculation | |
509 | */ | |
510 | if ( _oDatatable && typeof oSettings.oScroll != 'undefined' && | |
511 | (oSettings.oScroll.sX !== "" || oSettings.oScroll.sY !== "") ) | |
512 | { | |
513 | aiPos[1] -= $(oSettings.nTable.parentNode).scrollTop(); | |
514 | aiPos[0] -= $(oSettings.nTable.parentNode).scrollLeft(); | |
515 | } | |
516 | ||
517 | /* Correct viewport positioning for vertical scrolling */ | |
518 | if ( aiPos[1]+iHeight > iScrollTop+iViewportHeight ) | |
519 | { | |
520 | /* Displayed element if off the bottom of the viewport */ | |
521 | _fnSetScrollTop( aiPos[1]+iHeight - iViewportHeight ); | |
522 | } | |
523 | else if ( aiPos[1] < iScrollTop ) | |
524 | { | |
525 | /* Displayed element if off the top of the viewport */ | |
526 | _fnSetScrollTop( aiPos[1] ); | |
527 | } | |
528 | ||
529 | /* Correct viewport positioning for horizontal scrolling */ | |
530 | if ( aiPos[0]+iWidth > iScrollLeft+iViewportWidth ) | |
531 | { | |
532 | /* Displayed element is off the bottom of the viewport */ | |
533 | _fnSetScrollLeft( aiPos[0]+iWidth - iViewportWidth ); | |
534 | } | |
535 | else if ( aiPos[0] < iScrollLeft ) | |
536 | { | |
537 | /* Displayed element if off the Left of the viewport */ | |
538 | _fnSetScrollLeft( aiPos[0] ); | |
539 | } | |
540 | } | |
541 | ||
542 | /* Take account of scrolling in DataTables 1.7 */ | |
543 | if ( _oDatatable && typeof oSettings.oScroll != 'undefined' && | |
544 | (oSettings.oScroll.sX !== "" || oSettings.oScroll.sY !== "") ) | |
545 | { | |
546 | var dtScrollBody = oSettings.nTable.parentNode; | |
547 | iViewportHeight = dtScrollBody.clientHeight; | |
548 | iViewportWidth = dtScrollBody.clientWidth; | |
549 | iScrollTop = dtScrollBody.scrollTop; | |
550 | iScrollLeft = dtScrollBody.scrollLeft; | |
551 | iHeight = nTarget.offsetHeight; | |
552 | iWidth = nTarget.offsetWidth; | |
553 | ||
554 | /* Correct for vertical scrolling */ | |
555 | if ( nTarget.offsetTop + iHeight > iViewportHeight+iScrollTop ) | |
556 | { | |
557 | dtScrollBody.scrollTop = (nTarget.offsetTop + iHeight) - iViewportHeight; | |
558 | } | |
559 | else if ( nTarget.offsetTop < iScrollTop ) | |
560 | { | |
561 | dtScrollBody.scrollTop = nTarget.offsetTop; | |
562 | } | |
563 | ||
564 | /* Correct for horizontal scrolling */ | |
565 | if ( nTarget.offsetLeft + iWidth > iViewportWidth+iScrollLeft ) | |
566 | { | |
567 | dtScrollBody.scrollLeft = (nTarget.offsetLeft + iWidth) - iViewportWidth; | |
568 | } | |
569 | else if ( nTarget.offsetLeft < iScrollLeft ) | |
570 | { | |
571 | dtScrollBody.scrollLeft = nTarget.offsetLeft; | |
572 | } | |
573 | } | |
574 | ||
575 | /* Focused - so we want to capture the keys */ | |
576 | _fnCaptureKeys(); | |
577 | ||
578 | /* Fire of the focus event if there is one */ | |
579 | _fnEventFire( "focus", _iOldX, _iOldY ); | |
580 | } | |
581 | ||
582 | ||
583 | /* | |
584 | * Function: _fnBlur | |
585 | * Purpose: Blur focus from the whole table | |
586 | * Returns: - | |
587 | * Inputs: - | |
588 | */ | |
589 | function _fnBlur() | |
590 | { | |
591 | _fnRemoveFocus( _nOldFocus ); | |
592 | _iOldX = null; | |
593 | _iOldY = null; | |
594 | _nOldFocus = null; | |
595 | _fnReleaseKeys(); | |
596 | } | |
597 | ||
598 | ||
599 | /* | |
600 | * Function: _fnRemoveFocus | |
601 | * Purpose: Remove focus from a cell and fire any blur events which are attached | |
602 | * Returns: - | |
603 | * Inputs: node:nTarget - cell of interest | |
604 | */ | |
605 | function _fnRemoveFocus( nTarget ) | |
606 | { | |
607 | $(nTarget).removeClass( _sFocusClass ); | |
608 | $(nTarget).parent().removeClass( _sFocusClass ); | |
609 | _fnEventFire( "blur", _iOldX, _iOldY ); | |
610 | } | |
611 | ||
612 | ||
613 | /* | |
614 | * Function: _fnClick | |
615 | * Purpose: Focus on the element that has been clicked on by the user | |
616 | * Returns: - | |
617 | * Inputs: event:e - click event | |
618 | */ | |
619 | function _fnClick ( e ) | |
620 | { | |
621 | var nTarget = this; | |
622 | while ( nTarget.nodeName != "TD" ) | |
623 | { | |
624 | nTarget = nTarget.parentNode; | |
625 | } | |
626 | ||
627 | _fnSetFocus( nTarget ); | |
628 | _fnCaptureKeys(); | |
629 | } | |
630 | ||
631 | ||
632 | ||
633 | /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * | |
634 | * Key events | |
635 | */ | |
636 | ||
637 | /* | |
638 | * Function: _fnKey | |
639 | * Purpose: Deal with a key events, be it moving the focus or return etc. | |
640 | * Returns: bool: - allow browser default action | |
641 | * Inputs: event:e - key event | |
642 | */ | |
643 | function _fnKey ( e ) | |
644 | { | |
645 | /* If user or system has blocked KeyTable from doing anything, just ignore this event */ | |
646 | if ( _that.block || !_bKeyCapture ) | |
647 | { | |
648 | return true; | |
649 | } | |
650 | ||
651 | /* If a modifier key is pressed (exapct shift), ignore the event */ | |
652 | if ( e.metaKey || e.altKey || e.ctrlKey ) | |
653 | { | |
654 | return true; | |
655 | } | |
656 | var | |
657 | x, y, | |
658 | iTableWidth = _nBody.getElementsByTagName('tr')[0].getElementsByTagName('td').length, | |
659 | iTableHeight; | |
660 | ||
661 | /* Get table height and width - done here so as to be dynamic (if table is updated) */ | |
662 | if ( _oDatatable ) | |
663 | { | |
664 | /* | |
665 | * Locate the current node in the DataTable overriding the old positions - the reason for | |
666 | * is is that there might have been some DataTables interaction between the last focus and | |
667 | * now | |
668 | */ | |
669 | iTableHeight = _oDatatable.aiDisplay.length; | |
670 | ||
671 | var aDtPos = _fnFindDtCell( _nOldFocus ); | |
672 | if ( aDtPos === null ) | |
673 | { | |
674 | /* If the table has been updated such that the focused cell can't be seen - do nothing */ | |
675 | return; | |
676 | } | |
677 | _iOldX = aDtPos[ 0 ]; | |
678 | _iOldY = aDtPos[ 1 ]; | |
679 | } | |
680 | else | |
681 | { | |
682 | iTableHeight = _nBody.getElementsByTagName('tr').length; | |
683 | } | |
684 | ||
685 | /* Capture shift+tab to match the left arrow key */ | |
686 | var iKey = (e.keyCode == 9 && e.shiftKey) ? -1 : e.keyCode; | |
687 | ||
688 | switch( iKey ) | |
689 | { | |
690 | case 13: /* return */ | |
691 | e.preventDefault(); | |
692 | e.stopPropagation(); | |
693 | _fnEventFire( "action", _iOldX, _iOldY ); | |
694 | return true; | |
695 | ||
696 | case 27: /* esc */ | |
697 | if ( !_fnEventFire( "esc", _iOldX, _iOldY ) ) | |
698 | { | |
699 | /* Only lose focus if there isn't an escape handler on the cell */ | |
700 | _fnBlur(); | |
701 | return; | |
702 | } | |
703 | x = _iOldX; | |
704 | y = _iOldY; | |
705 | break; | |
706 | ||
707 | case -1: | |
708 | case 37: /* left arrow */ | |
709 | if ( _iOldX > 0 ) { | |
710 | x = _iOldX - 1; | |
711 | y = _iOldY; | |
712 | } else if ( _iOldY > 0 ) { | |
713 | x = iTableWidth-1; | |
714 | y = _iOldY - 1; | |
715 | } else { | |
716 | /* at start of table */ | |
717 | if ( iKey == -1 && _bForm ) | |
718 | { | |
719 | /* If we are in a form, return focus to the 'input' element such that tabbing will | |
720 | * follow correctly in the browser | |
721 | */ | |
722 | _bInputFocused = true; | |
723 | _nInput.focus(); | |
724 | ||
725 | /* This timeout is a little nasty - but IE appears to have some asyhnc behaviour for | |
726 | * focus | |
727 | */ | |
728 | setTimeout( function(){ _bInputFocused = false; }, 0 ); | |
729 | _bKeyCapture = false; | |
730 | _fnBlur(); | |
731 | return true; | |
732 | } | |
733 | else | |
734 | { | |
735 | return false; | |
736 | } | |
737 | } | |
738 | break; | |
739 | ||
740 | case 38: /* up arrow */ | |
741 | if ( _iOldY > 0 ) { | |
742 | x = _iOldX; | |
743 | y = _iOldY - 1; | |
744 | } else { | |
745 | return false; | |
746 | } | |
747 | break; | |
748 | ||
749 | case 36: /* home */ | |
750 | x = _iOldX; | |
751 | y = 0; | |
752 | break; | |
753 | ||
754 | case 33: /* page up */ | |
755 | x = _iOldX; | |
756 | y = _iOldY - 10; | |
757 | if (y < 0) { | |
758 | y = 0; | |
759 | } | |
760 | break; | |
761 | ||
762 | case 9: /* tab */ | |
763 | case 39: /* right arrow */ | |
764 | if ( _iOldX < iTableWidth-1 ) { | |
765 | x = _iOldX + 1; | |
766 | y = _iOldY; | |
767 | } else if ( _iOldY < iTableHeight-1 ) { | |
768 | x = 0; | |
769 | y = _iOldY + 1; | |
770 | } else { | |
771 | /* at end of table */ | |
772 | if ( iKey == 9 && _bForm ) | |
773 | { | |
774 | /* If we are in a form, return focus to the 'input' element such that tabbing will | |
775 | * follow correctly in the browser | |
776 | */ | |
777 | _bInputFocused = true; | |
778 | _nInput.focus(); | |
779 | ||
780 | /* This timeout is a little nasty - but IE appears to have some asyhnc behaviour for | |
781 | * focus | |
782 | */ | |
783 | setTimeout( function(){ _bInputFocused = false; }, 0 ); | |
784 | _bKeyCapture = false; | |
785 | _fnBlur(); | |
786 | return true; | |
787 | } | |
788 | else | |
789 | { | |
790 | return false; | |
791 | } | |
792 | } | |
793 | break; | |
794 | ||
795 | case 40: /* down arrow */ | |
796 | if ( _iOldY < iTableHeight-1 ) { | |
797 | x = _iOldX; | |
798 | y = _iOldY + 1; | |
799 | } else { | |
800 | return false; | |
801 | } | |
802 | break; | |
803 | ||
804 | case 35: /* end */ | |
805 | x = _iOldX; | |
806 | y = iTableHeight-1; | |
807 | break; | |
808 | ||
809 | case 34: /* page down */ | |
810 | x = _iOldX; | |
811 | y = _iOldY+10; | |
812 | if (y > iTableHeight-1) { | |
813 | y = iTableHeight-1; | |
814 | } | |
815 | break; | |
816 | ||
817 | default: /* Nothing we are interested in */ | |
818 | return true; | |
819 | } | |
820 | ||
821 | _fnSetFocus( _fnCellFromCoords(x, y) ); | |
822 | return false; | |
823 | } | |
824 | ||
825 | ||
826 | /* | |
827 | * Function: _fnCaptureKeys | |
828 | * Purpose: Start capturing key events for this table | |
829 | * Returns: - | |
830 | * Inputs: - | |
831 | */ | |
832 | function _fnCaptureKeys( ) | |
833 | { | |
834 | if ( !_bKeyCapture ) | |
835 | { | |
836 | _bKeyCapture = true; | |
837 | } | |
838 | } | |
839 | ||
840 | ||
841 | /* | |
842 | * Function: _fnReleaseKeys | |
843 | * Purpose: Stop capturing key events for this table | |
844 | * Returns: - | |
845 | * Inputs: - | |
846 | */ | |
847 | function _fnReleaseKeys( ) | |
848 | { | |
849 | _bKeyCapture = false; | |
850 | } | |
851 | ||
852 | ||
853 | ||
854 | /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * | |
855 | * Support functions | |
856 | */ | |
857 | ||
858 | /* | |
859 | * Function: _fnCellFromCoords | |
860 | * Purpose: Calulate the target TD cell from x and y coordinates | |
861 | * Returns: node: - TD target | |
862 | * Inputs: int:x - x coordinate | |
863 | * int:y - y coordinate | |
864 | */ | |
865 | function _fnCellFromCoords( x, y ) | |
866 | { | |
867 | if ( _oDatatable ) | |
868 | { | |
869 | if ( typeof _oDatatable.aoData[ _oDatatable.aiDisplay[ y ] ] != 'undefined' ) | |
870 | { | |
871 | return _oDatatable.aoData[ _oDatatable.aiDisplay[ y ] ].nTr.getElementsByTagName('td')[x]; | |
872 | } | |
873 | else | |
874 | { | |
875 | return null; | |
876 | } | |
877 | } | |
878 | else | |
879 | { | |
880 | return $('tr:eq('+y+')>td:eq('+x+')', _nBody )[0]; | |
881 | } | |
882 | } | |
883 | ||
884 | ||
885 | /* | |
886 | * Function: _fnCoordsFromCell | |
887 | * Purpose: Calculate the x and y position in a table from a TD cell | |
888 | * Returns: array[2] int: [x, y] | |
889 | * Inputs: node:n - TD cell of interest | |
890 | * Notes: Not actually interested in this for DataTables since it might go out of date | |
891 | */ | |
892 | function _fnCoordsFromCell( n ) | |
893 | { | |
894 | if ( _oDatatable ) | |
895 | { | |
896 | return [ | |
897 | $('td', n.parentNode).index(n), | |
898 | $('tr', n.parentNode.parentNode).index(n.parentNode) + _oDatatable._iDisplayStart | |
899 | ]; | |
900 | } | |
901 | else | |
902 | { | |
903 | return [ | |
904 | $('td', n.parentNode).index(n), | |
905 | $('tr', n.parentNode.parentNode).index(n.parentNode) | |
906 | ]; | |
907 | } | |
908 | } | |
909 | ||
910 | ||
911 | /* | |
912 | * Function: _fnSetScrollTop | |
913 | * Purpose: Set the vertical scrolling position | |
914 | * Returns: - | |
915 | * Inputs: int:iPos - scrolltop | |
916 | * Notes: This is so nasty, but without browser detection you can't tell which you should set | |
917 | * So on browsers that support both, the scroll top will be set twice. I can live with | |
918 | * that :-) | |
919 | */ | |
920 | function _fnSetScrollTop( iPos ) | |
921 | { | |
922 | document.documentElement.scrollTop = iPos; | |
923 | document.body.scrollTop = iPos; | |
924 | } | |
925 | ||
926 | ||
927 | /* | |
928 | * Function: _fnSetScrollLeft | |
929 | * Purpose: Set the horizontal scrolling position | |
930 | * Returns: - | |
931 | * Inputs: int:iPos - scrollleft | |
932 | */ | |
933 | function _fnSetScrollLeft( iPos ) | |
934 | { | |
935 | document.documentElement.scrollLeft = iPos; | |
936 | document.body.scrollLeft = iPos; | |
937 | } | |
938 | ||
939 | ||
940 | /* | |
941 | * Function: _fnGetPos | |
942 | * Purpose: Get the position of an object on the rendered page | |
943 | * Returns: array[2] int: [left, right] | |
944 | * Inputs: node:obj - element of interest | |
945 | */ | |
946 | function _fnGetPos ( obj ) | |
947 | { | |
948 | var iLeft = 0; | |
949 | var iTop = 0; | |
950 | ||
951 | if (obj.offsetParent) | |
952 | { | |
953 | iLeft = obj.offsetLeft; | |
954 | iTop = obj.offsetTop; | |
955 | obj = obj.offsetParent; | |
956 | while (obj) | |
957 | { | |
958 | iLeft += obj.offsetLeft; | |
959 | iTop += obj.offsetTop; | |
960 | obj = obj.offsetParent; | |
961 | } | |
962 | } | |
963 | return [iLeft,iTop]; | |
964 | } | |
965 | ||
966 | ||
967 | /* | |
968 | * Function: _fnFindDtCell | |
969 | * Purpose: Get the coords. of a cell from the DataTables internal information | |
970 | * Returns: array[2] int: [x, y] coords. or null if not found | |
971 | * Inputs: node:nTarget - the node of interest | |
972 | */ | |
973 | function _fnFindDtCell( nTarget ) | |
974 | { | |
975 | for ( var i=0, iLen=_oDatatable.aiDisplay.length ; i<iLen ; i++ ) | |
976 | { | |
977 | var nTr = _oDatatable.aoData[ _oDatatable.aiDisplay[i] ].nTr; | |
978 | var nTds = nTr.getElementsByTagName('td'); | |
979 | for ( var j=0, jLen=nTds.length ; j<jLen ; j++ ) | |
980 | { | |
981 | if ( nTds[j] == nTarget ) | |
982 | { | |
983 | return [ j, i ]; | |
984 | } | |
985 | } | |
986 | } | |
987 | return null; | |
988 | } | |
989 | ||
990 | ||
991 | ||
992 | /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * | |
993 | * Initialisation | |
994 | */ | |
995 | ||
996 | /* | |
997 | * Function: _fnInit | |
998 | * Purpose: Initialise the KeyTable | |
999 | * Returns: - | |
1000 | * Inputs: object:oInit - optional - Initalisation object with the following parameters: | |
1001 | * array[2] int:focus - x and y coordinates of the initial target | |
1002 | * or | |
1003 | * node:focus - the node to set initial focus on | |
1004 | * node:table - the table to use, if not given, first table with class 'KeyTable' will be used | |
1005 | * string:focusClass - focusing class to give to table elements | |
1006 | * object:that - focus | |
1007 | * bool:initScroll - scroll the view port on load, default true | |
1008 | * int:tabIndex - the tab index to give the hidden input element | |
1009 | */ | |
1010 | function _fnInit( table, datatable, oInit, that ) | |
1011 | { | |
1012 | /* Save scope */ | |
1013 | _that = that; | |
1014 | ||
1015 | /* Capture undefined initialisation and apply the defaults */ | |
1016 | if ( typeof oInit == 'undefined' ) { | |
1017 | oInit = {}; | |
1018 | } | |
1019 | ||
1020 | if ( typeof oInit.focus == 'undefined' ) { | |
1021 | oInit.focus = [0,0]; | |
1022 | } | |
1023 | ||
1024 | oInit.table = table; | |
1025 | $(oInit.table).addClass('KeyTable'); | |
1026 | ||
1027 | if ( typeof oInit.focusClass != 'undefined' ) { | |
1028 | _sFocusClass = oInit.focusClass; | |
1029 | } | |
1030 | ||
1031 | if ( typeof datatable != 'undefined' ) { | |
1032 | _oDatatable = datatable; | |
1033 | } | |
1034 | ||
1035 | if ( typeof oInit.initScroll == 'undefined' ) { | |
1036 | oInit.initScroll = true; | |
1037 | } | |
1038 | ||
1039 | if ( typeof oInit.form == 'undefined' ) { | |
1040 | oInit.form = false; | |
1041 | } | |
1042 | _bForm = oInit.form; | |
1043 | ||
1044 | /* Cache the tbody node of interest */ | |
1045 | _nBody = oInit.table.getElementsByTagName('tbody')[0]; | |
1046 | ||
1047 | /* If the table is inside a form, then we need a hidden input box which can be used by the | |
1048 | * browser to catch the browser tabbing for our table | |
1049 | */ | |
1050 | if ( _bForm ) | |
1051 | { | |
1052 | var nDiv = document.createElement('div'); | |
1053 | _nInput = document.createElement('input'); | |
1054 | nDiv.style.height = "1px"; /* Opera requires a little something */ | |
1055 | nDiv.style.width = "0px"; | |
1056 | nDiv.style.overflow = "hidden"; | |
1057 | if ( typeof oInit.tabIndex != 'undefined' ) | |
1058 | { | |
1059 | _nInput.tabIndex = oInit.tabIndex; | |
1060 | } | |
1061 | nDiv.appendChild(_nInput); | |
1062 | oInit.table.parentNode.insertBefore( nDiv, oInit.table.nextSibling ); | |
1063 | ||
1064 | $(_nInput).focus( function () { | |
1065 | /* See if we want to 'tab into' the table or out */ | |
1066 | if ( !_bInputFocused ) | |
1067 | { | |
1068 | _bKeyCapture = true; | |
1069 | _bInputFocused = false; | |
1070 | if ( typeof oInit.focus.nodeName != "undefined" ) | |
1071 | { | |
1072 | _fnSetFocus( oInit.focus, oInit.initScroll ); | |
1073 | } | |
1074 | else | |
1075 | { | |
1076 | _fnSetFocus( _fnCellFromCoords( oInit.focus[0], oInit.focus[1]), oInit.initScroll ); | |
1077 | } | |
1078 | ||
1079 | /* Need to interup the thread for this to work */ | |
1080 | setTimeout( function() { _nInput.blur(); }, 0 ); | |
1081 | } | |
1082 | } ); | |
1083 | _bKeyCapture = false; | |
1084 | } | |
1085 | else | |
1086 | { | |
1087 | /* Set the initial focus on the table */ | |
1088 | if ( typeof oInit.focus.nodeName != "undefined" ) | |
1089 | { | |
1090 | _fnSetFocus( oInit.focus, oInit.initScroll ); | |
1091 | } | |
1092 | else | |
1093 | { | |
1094 | _fnSetFocus( _fnCellFromCoords( oInit.focus[0], oInit.focus[1]), oInit.initScroll ); | |
1095 | } | |
1096 | _fnCaptureKeys(); | |
1097 | } | |
1098 | ||
1099 | /* Add event listeners */ | |
1100 | $(document).bind( "keydown", _fnKey ); | |
1101 | ||
1102 | if ( _oDatatable ) | |
1103 | { | |
1104 | $(_oDatatable.nTable).on( 'click', 'td', _fnClick ); | |
1105 | } | |
1106 | else | |
1107 | { | |
1108 | $(_nBody).on( 'click', 'td', _fnClick ); | |
1109 | } | |
1110 | ||
1111 | /* Loose table focus when click outside the table */ | |
1112 | $(document).click( function(e) { | |
1113 | var nTarget = e.target; | |
1114 | var bTableClick = false; | |
1115 | while ( nTarget ) | |
1116 | { | |
1117 | if ( nTarget == oInit.table ) | |
1118 | { | |
1119 | bTableClick = true; | |
1120 | break; | |
1121 | } | |
1122 | nTarget = nTarget.parentNode; | |
1123 | } | |
1124 | if ( !bTableClick ) | |
1125 | { | |
1126 | _fnBlur(); | |
1127 | } | |
1128 | } ); | |
1129 | } | |
1130 | ||
1131 | var table, datatable; | |
1132 | ||
1133 | if ( oInit === undefined ) { | |
1134 | table = $('table.KeyTable')[0]; | |
1135 | datatable = null; | |
1136 | } | |
1137 | else if ( $.isPlainObject( oInit ) ) { | |
1138 | table = oInit.table; | |
1139 | datatable = oInit.datatable; | |
1140 | } | |
1141 | else { | |
1142 | datatable = new $.fn.dataTable.Api( oInit ).settings()[0]; | |
1143 | table = datatable.nTable; | |
1144 | } | |
1145 | /* Initialise our new object */ | |
1146 | _fnInit( table, datatable, oInit, this ); | |
1147 | }; | |
1148 | ||
1149 | ||
1150 | KeyTable.version = "1.2.1"; | |
1151 | ||
1152 | ||
1153 | $.fn.dataTable.KeyTable = KeyTable; | |
1154 | $.fn.DataTable.KeyTable = KeyTable; | |
1155 | ||
1156 | ||
1157 | return KeyTable; | |
1158 | }; // /factory | |
1159 | ||
1160 | ||
1161 | // Define as an AMD module if possible | |
1162 | if ( typeof define === 'function' && define.amd ) { | |
1163 | define( ['jquery', 'datatables'], factory ); | |
1164 | } | |
1165 | else if ( typeof exports === 'object' ) { | |
1166 | // Node/CommonJS | |
1167 | factory( require('jquery'), require('datatables') ); | |
1168 | } | |
1169 | else if ( jQuery && !jQuery.fn.dataTable.KeyTable ) { | |
1170 | // Otherwise simply initialise as normal, stopping multiple evaluation | |
1171 | factory( jQuery, jQuery.fn.dataTable ); | |
1172 | } | |
1173 | ||
1174 | ||
1175 | })(window, document); |