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