1
2/*! taboverride v4.0.2 | https://github.com/wjbryant/taboverride
3(c) 2014 Bill Bryant | http://opensource.org/licenses/mit */
4
5/**
6 * @fileOverview taboverride
7 * @author       Bill Bryant
8 * @version      4.0.2
9 */
10
11/*jslint browser: true */
12/*global exports, define */
13
14// use CommonJS or AMD if available
15(function (factory) {
16    'use strict';
17
18    var mod;
19
20    if (typeof exports === 'object') {
21        // Node.js/CommonJS
22        factory(exports);
23    } else if (typeof define === 'function' && define.amd) {
24        // AMD - register as an anonymous module
25        // files must be concatenated using an AMD-aware tool such as r.js
26        define(['exports'], factory);
27    } else {
28        // no module format - create global variable
29        mod = window.tabOverride = {};
30        factory(mod);
31    }
32}(function (tabOverride) {
33    'use strict';
34
35    /**
36     * The tabOverride namespace object
37     *
38     * @namespace tabOverride
39     */
40
41    var document = window.document,
42        listeners,
43        aTab = '\t', // the string representing a tab
44        tabKey = 9,
45        untabKey = 9,
46        tabModifierKeys = [],
47        untabModifierKeys = ['shiftKey'],
48        autoIndent = true, // whether each line should be automatically indented
49        inWhitespace = false, // whether the start of the selection is in the leading whitespace on enter
50        textareaElem = document.createElement('textarea'), // temp textarea element to get newline character(s)
51        newline, // the newline character sequence (\n or \r\n)
52        newlineLen, // the number of characters used for a newline (1 or 2)
53        hooks = {};
54
55    /**
56     * Determines whether the specified modifier keys match the modifier keys
57     * that were pressed.
58     *
59     * @param  {string[]} modifierKeys  the modifier keys to check - ex: ['shiftKey']
60     * @param  {Event}    e             the event object for the keydown event
61     * @return {boolean}                whether modifierKeys are valid for the event
62     *
63     * @method tabOverride.utils.isValidModifierKeyCombo
64     */
65    function isValidModifierKeyCombo(modifierKeys, e) {
66        var modifierKeyNames = ['alt', 'ctrl', 'meta', 'shift'],
67            numModKeys = modifierKeys.length,
68            i,
69            j,
70            currModifierKey,
71            isValid = true;
72
73        // check that all required modifier keys were pressed
74        for (i = 0; i < numModKeys; i += 1) {
75            if (!e[modifierKeys[i]]) {
76                isValid = false;
77                break;
78            }
79        }
80
81        // if the requirements were met, check for additional modifier keys
82        if (isValid) {
83            for (i = 0; i < modifierKeyNames.length; i += 1) {
84                currModifierKey = modifierKeyNames[i] + 'Key';
85
86                // if this key was pressed
87                if (e[currModifierKey]) {
88                    // if there are required keys, check whether the current key
89                    // is required
90                    if (numModKeys) {
91                        isValid = false;
92
93                        // if this is a required key, continue
94                        for (j = 0; j < numModKeys; j += 1) {
95                            if (currModifierKey === modifierKeys[j]) {
96                                isValid = true;
97                                break;
98                            }
99                        }
100                    } else {
101                        // no required keys, but one was pressed
102                        isValid = false;
103                    }
104                }
105
106                // an extra key was pressed, don't check anymore
107                if (!isValid) {
108                    break;
109                }
110            }
111        }
112
113        return isValid;
114    }
115
116    /**
117     * Determines whether the tab key combination was pressed.
118     *
119     * @param  {number}  keyCode  the key code of the key that was pressed
120     * @param  {Event}   e        the event object for the key event
121     * @return {boolean}          whether the tab key combo was pressed
122     *
123     * @private
124     */
125    function tabKeyComboPressed(keyCode, e) {
126        return keyCode === tabKey && isValidModifierKeyCombo(tabModifierKeys, e);
127    }
128
129    /**
130     * Determines whether the untab key combination was pressed.
131     *
132     * @param  {number}  keyCode  the key code of the key that was pressed
133     * @param  {Event}   e        the event object for the key event
134     * @return {boolean}          whether the untab key combo was pressed
135     *
136     * @private
137     */
138    function untabKeyComboPressed(keyCode, e) {
139        return keyCode === untabKey && isValidModifierKeyCombo(untabModifierKeys, e);
140    }
141
142    /**
143     * Creates a function to get and set the specified key combination.
144     *
145     * @param  {Function} keyFunc       getter/setter function for the key
146     * @param  {string[]} modifierKeys  the array of modifier keys to manipulate
147     * @return {Function}               a getter/setter function for the specified
148     *                                  key combination
149     *
150     * @private
151     */
152    function createKeyComboFunction(keyFunc, modifierKeys) {
153        return function (keyCode, modifierKeyNames) {
154            var i,
155                keyCombo = '';
156
157            if (arguments.length) {
158                if (typeof keyCode === 'number') {
159                    keyFunc(keyCode);
160
161                    modifierKeys.length = 0; // clear the array
162
163                    if (modifierKeyNames && modifierKeyNames.length) {
164                        for (i = 0; i < modifierKeyNames.length; i += 1) {
165                            modifierKeys.push(modifierKeyNames[i] + 'Key');
166                        }
167                    }
168                }
169
170                return this;
171            }
172
173            for (i = 0; i < modifierKeys.length; i += 1) {
174                keyCombo += modifierKeys[i].slice(0, -3) + '+';
175            }
176
177            return keyCombo + keyFunc();
178        };
179    }
180
181    /**
182     * Event handler to insert or remove tabs and newlines on the keydown event
183     * for the tab or enter key.
184     *
185     * @param {Event} e  the event object
186     *
187     * @method tabOverride.handlers.keydown
188     */
189    function overrideKeyDown(e) {
190        e = e || event;
191
192        // textarea elements can only contain text nodes which don't receive
193        // keydown events, so the event target/srcElement will always be the
194        // textarea element, however, prefer currentTarget in order to support
195        // delegated events in compliant browsers
196        var target = e.currentTarget || e.srcElement, // don't use the "this" keyword (doesn't work in old IE)
197            key = e.keyCode, // the key code for the key that was pressed
198            tab, // the string representing a tab
199            tabLen, // the length of a tab
200            text, // initial text in the textarea
201            range, // the IE TextRange object
202            tempRange, // used to calculate selection start and end positions in IE
203            preNewlines, // the number of newline character sequences before the selection start (for IE)
204            selNewlines, // the number of newline character sequences within the selection (for IE)
205            initScrollTop, // initial scrollTop value used to fix scrolling in Firefox
206            selStart, // the selection start position
207            selEnd, // the selection end position
208            sel, // the selected text
209            startLine, // for multi-line selections, the first character position of the first line
210            endLine, // for multi-line selections, the last character position of the last line
211            numTabs, // the number of tabs inserted / removed in the selection
212            startTab, // if a tab was removed from the start of the first line
213            preTab, // if a tab was removed before the start of the selection
214            whitespace, // the whitespace at the beginning of the first selected line
215            whitespaceLen, // the length of the whitespace at the beginning of the first selected line
216            CHARACTER = 'character'; // string constant used for the Range.move methods
217
218        // don't do any unnecessary work
219        if ((target.nodeName && target.nodeName.toLowerCase() !== 'textarea') ||
220                (key !== tabKey && key !== untabKey && (key !== 13 || !autoIndent))) {
221            return;
222        }
223
224        // initialize variables used for tab and enter keys
225        inWhitespace = false; // this will be set to true if enter is pressed in the leading whitespace
226        text = target.value;
227
228        // this is really just for Firefox, but will be used by all browsers that support
229        // selectionStart and selectionEnd - whenever the textarea value property is reset,
230        // Firefox scrolls back to the top - this is used to set it back to the original value
231        // scrollTop is nonstandard, but supported by all modern browsers
232        initScrollTop = target.scrollTop;
233
234        // get the text selection
235        if (typeof target.selectionStart === 'number') {
236            selStart = target.selectionStart;
237            selEnd = target.selectionEnd;
238            sel = text.slice(selStart, selEnd);
239
240        } else if (document.selection) { // IE
241            range = document.selection.createRange();
242            sel = range.text;
243            tempRange = range.duplicate();
244            tempRange.moveToElementText(target);
245            tempRange.setEndPoint('EndToEnd', range);
246            selEnd = tempRange.text.length;
247            selStart = selEnd - sel.length;
248
249            // whenever the value of the textarea is changed, the range needs to be reset
250            // IE <9 (and Opera) use both \r and \n for newlines - this adds an extra character
251            // that needs to be accounted for when doing position calculations with ranges
252            // these values are used to offset the selection start and end positions
253            if (newlineLen > 1) {
254                preNewlines = text.slice(0, selStart).split(newline).length - 1;
255                selNewlines = sel.split(newline).length - 1;
256            } else {
257                preNewlines = selNewlines = 0;
258            }
259        } else {
260            return; // cannot access textarea selection - do nothing
261        }
262
263        // tab / untab key - insert / remove tab
264        if (key === tabKey || key === untabKey) {
265
266            // initialize tab variables
267            tab = aTab;
268            tabLen = tab.length;
269            numTabs = 0;
270            startTab = 0;
271            preTab = 0;
272
273            // multi-line selection
274            if (selStart !== selEnd && sel.indexOf('\n') !== -1) {
275                // for multiple lines, only insert / remove tabs from the beginning of each line
276
277                // find the start of the first selected line
278                if (selStart === 0 || text.charAt(selStart - 1) === '\n') {
279                    // the selection starts at the beginning of a line
280                    startLine = selStart;
281                } else {
282                    // the selection starts after the beginning of a line
283                    // set startLine to the beginning of the first partially selected line
284                    // subtract 1 from selStart in case the cursor is at the newline character,
285                    // for instance, if the very end of the previous line was selected
286                    // add 1 to get the next character after the newline
287                    // if there is none before the selection, lastIndexOf returns -1
288                    // when 1 is added to that it becomes 0 and the first character is used
289                    startLine = text.lastIndexOf('\n', selStart - 1) + 1;
290                }
291
292                // find the end of the last selected line
293                if (selEnd === text.length || text.charAt(selEnd) === '\n') {
294                    // the selection ends at the end of a line
295                    endLine = selEnd;
296                } else if (text.charAt(selEnd - 1) === '\n') {
297                    // the selection ends at the start of a line, but no
298                    // characters are selected - don't indent this line
299                    endLine = selEnd - 1;
300                } else {
301                    // the selection ends before the end of a line
302                    // set endLine to the end of the last partially selected line
303                    endLine = text.indexOf('\n', selEnd);
304                    if (endLine === -1) {
305                        endLine = text.length;
306                    }
307                }
308
309                // tab key combo - insert tabs
310                if (tabKeyComboPressed(key, e)) {
311
312                    numTabs = 1; // for the first tab
313
314                    // insert tabs at the beginning of each line of the selection
315                    target.value = text.slice(0, startLine) + tab +
316                        text.slice(startLine, endLine).replace(/\n/g, function () {
317                            numTabs += 1;
318                            return '\n' + tab;
319                        }) + text.slice(endLine);
320
321                    // set start and end points
322                    if (range) { // IE
323                        range.collapse();
324                        range.moveEnd(CHARACTER, selEnd + (numTabs * tabLen) - selNewlines - preNewlines);
325                        range.moveStart(CHARACTER, selStart + tabLen - preNewlines);
326                        range.select();
327                    } else {
328                        // the selection start is always moved by 1 character
329                        target.selectionStart = selStart + tabLen;
330                        // move the selection end over by the total number of tabs inserted
331                        target.selectionEnd = selEnd + (numTabs * tabLen);
332                        target.scrollTop = initScrollTop;
333                    }
334                } else if (untabKeyComboPressed(key, e)) {
335                    // if the untab key combo was pressed, remove tabs instead of inserting them
336
337                    if (text.slice(startLine).indexOf(tab) === 0) {
338                        // is this tab part of the selection?
339                        if (startLine === selStart) {
340                            // it is, remove it
341                            sel = sel.slice(tabLen);
342                        } else {
343                            // the tab comes before the selection
344                            preTab = tabLen;
345                        }
346                        startTab = tabLen;
347                    }
348
349                    target.value = text.slice(0, startLine) + text.slice(startLine + preTab, selStart) +
350                        sel.replace(new RegExp('\n' + tab, 'g'), function () {
351                            numTabs += 1;
352                            return '\n';
353                        }) + text.slice(selEnd);
354
355                    // set start and end points
356                    if (range) { // IE
357                        // setting end first makes calculations easier
358                        range.collapse();
359                        range.moveEnd(CHARACTER, selEnd - startTab - (numTabs * tabLen) - selNewlines - preNewlines);
360                        range.moveStart(CHARACTER, selStart - preTab - preNewlines);
361                        range.select();
362                    } else {
363                        // set start first for Opera
364                        target.selectionStart = selStart - preTab; // preTab is 0 or tabLen
365                        // move the selection end over by the total number of tabs removed
366                        target.selectionEnd = selEnd - startTab - (numTabs * tabLen);
367                    }
368                } else {
369                    return; // do nothing for invalid key combinations
370                }
371
372            } else { // single line selection
373
374                // tab key combo - insert a tab
375                if (tabKeyComboPressed(key, e)) {
376                    if (range) { // IE
377                        range.text = tab;
378                        range.select();
379                    } else {
380                        target.value = text.slice(0, selStart) + tab + text.slice(selEnd);
381                        target.selectionEnd = target.selectionStart = selStart + tabLen;
382                        target.scrollTop = initScrollTop;
383                    }
384                } else if (untabKeyComboPressed(key, e)) {
385                    // if the untab key combo was pressed, remove a tab instead of inserting one
386
387                    // if the character before the selection is a tab, remove it
388                    if (text.slice(selStart - tabLen).indexOf(tab) === 0) {
389                        target.value = text.slice(0, selStart - tabLen) + text.slice(selStart);
390
391                        // set start and end points
392                        if (range) { // IE
393                            // collapses range and moves it by -1 tab
394                            range.move(CHARACTER, selStart - tabLen - preNewlines);
395                            range.select();
396                        } else {
397                            target.selectionEnd = target.selectionStart = selStart - tabLen;
398                            target.scrollTop = initScrollTop;
399                        }
400                    }
401                } else {
402                    return; // do nothing for invalid key combinations
403                }
404            }
405        } else if (autoIndent) { // Enter key
406            // insert a newline and copy the whitespace from the beginning of the line
407
408            // find the start of the first selected line
409            if (selStart === 0 || text.charAt(selStart - 1) === '\n') {
410                // the selection starts at the beginning of a line
411                // do nothing special
412                inWhitespace = true;
413                return;
414            }
415
416            // see explanation under "multi-line selection" above
417            startLine = text.lastIndexOf('\n', selStart - 1) + 1;
418
419            // find the end of the first selected line
420            endLine = text.indexOf('\n', selStart);
421
422            // if no newline is found, set endLine to the end of the text
423            if (endLine === -1) {
424                endLine = text.length;
425            }
426
427            // get the whitespace at the beginning of the first selected line (spaces and tabs only)
428            whitespace = text.slice(startLine, endLine).match(/^[ \t]*/)[0];
429            whitespaceLen = whitespace.length;
430
431            // the cursor (selStart) is in the whitespace at beginning of the line
432            // do nothing special
433            if (selStart < startLine + whitespaceLen) {
434                inWhitespace = true;
435                return;
436            }
437
438            if (range) { // IE
439                // insert the newline and whitespace
440                range.text = '\n' + whitespace;
441                range.select();
442            } else {
443                // insert the newline and whitespace
444                target.value = text.slice(0, selStart) + '\n' + whitespace + text.slice(selEnd);
445                // Opera uses \r\n for a newline, instead of \n,
446                // so use newlineLen instead of a hard-coded value
447                target.selectionEnd = target.selectionStart = selStart + newlineLen + whitespaceLen;
448                target.scrollTop = initScrollTop;
449            }
450        }
451
452        if (e.preventDefault) {
453            e.preventDefault();
454        } else {
455            e.returnValue = false;
456            return false;
457        }
458    }
459
460    /**
461     * Event handler to prevent the default action for the keypress event when
462     * tab or enter is pressed. Opera and Firefox also fire a keypress event
463     * when the tab or enter key is pressed. Opera requires that the default
464     * action be prevented on this event or the textarea will lose focus.
465     *
466     * @param {Event} e  the event object
467     *
468     * @method tabOverride.handlers.keypress
469     */
470    function overrideKeyPress(e) {
471        e = e || event;
472
473        var key = e.keyCode;
474
475        if (tabKeyComboPressed(key, e) || untabKeyComboPressed(key, e) ||
476                (key === 13 && autoIndent && !inWhitespace)) {
477
478            if (e.preventDefault) {
479                e.preventDefault();
480            } else {
481                e.returnValue = false;
482                return false;
483            }
484        }
485    }
486
487    /**
488     * Executes all registered extension functions for the specified hook.
489     *
490     * @param {string} hook    the name of the hook for which the extensions are registered
491     * @param {Array}  [args]  the arguments to pass to the extension
492     *
493     * @method tabOverride.utils.executeExtensions
494     */
495    function executeExtensions(hook, args) {
496        var i,
497            extensions = hooks[hook] || [],
498            len = extensions.length;
499
500        for (i = 0; i < len; i += 1) {
501            extensions[i].apply(null, args);
502        }
503    }
504
505    /**
506     * @typedef {Object} tabOverride.utils~handlerObj
507     *
508     * @property {string}   type     the event type
509     * @property {Function} handler  the handler function - passed an Event object
510     */
511
512    /**
513     * @typedef {Object} tabOverride.utils~listenersObj
514     *
515     * @property {Function} add     Adds all the event listeners to the
516     *                              specified element
517     * @property {Function} remove  Removes all the event listeners from
518     *                              the specified element
519     */
520
521    /**
522     * Creates functions to add and remove event listeners in a cross-browser
523     * compatible way.
524     *
525     * @param  {tabOverride.utils~handlerObj[]} handlerList  an array of {@link tabOverride.utils~handlerObj handlerObj} objects
526     * @return {tabOverride.utils~listenersObj}              a listenersObj object used to add and remove the event listeners
527     *
528     * @method tabOverride.utils.createListeners
529     */
530    function createListeners(handlerList) {
531        var i,
532            len = handlerList.length,
533            remove,
534            add;
535
536        function loop(func) {
537            for (i = 0; i < len; i += 1) {
538                func(handlerList[i].type, handlerList[i].handler);
539            }
540        }
541
542        // use the standard event handler registration method when available
543        if (document.addEventListener) {
544            remove = function (elem) {
545                loop(function (type, handler) {
546                    elem.removeEventListener(type, handler, false);
547                });
548            };
549            add = function (elem) {
550                // remove listeners before adding them to make sure they are not
551                // added more than once
552                remove(elem);
553                loop(function (type, handler) {
554                    elem.addEventListener(type, handler, false);
555                });
556            };
557        } else if (document.attachEvent) {
558            // support IE 6-8
559            remove = function (elem) {
560                loop(function (type, handler) {
561                    elem.detachEvent('on' + type, handler);
562                });
563            };
564            add = function (elem) {
565                remove(elem);
566                loop(function (type, handler) {
567                    elem.attachEvent('on' + type, handler);
568                });
569            };
570        }
571
572        return {
573            add: add,
574            remove: remove
575        };
576    }
577
578    /**
579     * Adds the Tab Override event listeners to the specified element.
580     *
581     * Hooks: addListeners - passed the element to which the listeners will
582     * be added.
583     *
584     * @param {Element} elem  the element to which the listeners will be added
585     *
586     * @method tabOverride.utils.addListeners
587     */
588    function addListeners(elem) {
589        executeExtensions('addListeners', [elem]);
590        listeners.add(elem);
591    }
592
593    /**
594     * Removes the Tab Override event listeners from the specified element.
595     *
596     * Hooks: removeListeners - passed the element from which the listeners
597     * will be removed.
598     *
599     * @param {Element} elem  the element from which the listeners will be removed
600     *
601     * @method tabOverride.utils.removeListeners
602     */
603    function removeListeners(elem) {
604        executeExtensions('removeListeners', [elem]);
605        listeners.remove(elem);
606    }
607
608
609    // Initialize Variables
610
611    listeners = createListeners([
612        { type: 'keydown', handler: overrideKeyDown },
613        { type: 'keypress', handler: overrideKeyPress }
614    ]);
615
616    // get the characters used for a newline
617    textareaElem.value = '\n';
618    newline = textareaElem.value;
619    newlineLen = newline.length;
620    textareaElem = null;
621
622
623    // Public Properties and Methods
624
625    /**
626     * Namespace for utility methods
627     *
628     * @namespace
629     */
630    tabOverride.utils = {
631        executeExtensions: executeExtensions,
632        isValidModifierKeyCombo: isValidModifierKeyCombo,
633        createListeners: createListeners,
634        addListeners: addListeners,
635        removeListeners: removeListeners
636    };
637
638    /**
639     * Namespace for event handler functions
640     *
641     * @namespace
642     */
643    tabOverride.handlers = {
644        keydown: overrideKeyDown,
645        keypress: overrideKeyPress
646    };
647
648    /**
649     * Adds an extension function to be executed when the specified hook is
650     * "fired." The extension function is called for each element and is passed
651     * any relevant arguments for the hook.
652     *
653     * @param  {string}   hook  the name of the hook for which the extension
654     *                          will be registered
655     * @param  {Function} func  the function to be executed when the hook is "fired"
656     * @return {Object}         the tabOverride object
657     */
658    tabOverride.addExtension = function (hook, func) {
659        if (hook && typeof hook === 'string' && typeof func === 'function') {
660            if (!hooks[hook]) {
661                hooks[hook] = [];
662            }
663            hooks[hook].push(func);
664        }
665
666        return this;
667    };
668
669    /**
670     * Enables or disables Tab Override for the specified textarea element(s).
671     *
672     * Hooks: set - passed the current element and a boolean indicating whether
673     * Tab Override was enabled or disabled.
674     *
675     * @param  {Element|Element[]} elems          the textarea element(s) for
676     *                                            which to enable or disable
677     *                                            Tab Override
678     * @param  {boolean}           [enable=true]  whether Tab Override should be
679     *                                            enabled for the element(s)
680     * @return {Object}                           the tabOverride object
681     */
682    tabOverride.set = function (elems, enable) {
683        var enableFlag,
684            elemsArr,
685            numElems,
686            setListeners,
687            attrValue,
688            i,
689            elem;
690
691        if (elems) {
692            enableFlag = arguments.length < 2 || enable;
693
694            // don't manipulate param when referencing arguments object
695            // this is just a matter of practice
696            elemsArr = elems;
697            numElems = elemsArr.length;
698
699            if (typeof numElems !== 'number') {
700                elemsArr = [elemsArr];
701                numElems = 1;
702            }
703
704            if (enableFlag) {
705                setListeners = addListeners;
706                attrValue = 'true';
707            } else {
708                setListeners = removeListeners;
709                attrValue = '';
710            }
711
712            for (i = 0; i < numElems; i += 1) {
713                elem = elemsArr[i];
714                if (elem && elem.nodeName && elem.nodeName.toLowerCase() === 'textarea') {
715                    executeExtensions('set', [elem, enableFlag]);
716                    elem.setAttribute('data-taboverride-enabled', attrValue);
717                    setListeners(elem);
718                }
719            }
720        }
721
722        return this;
723    };
724
725    /**
726     * Gets or sets the tab size for all elements that have Tab Override enabled.
727     * 0 represents the tab character.
728     *
729     * @param  {number}        [size]  the tab size
730     * @return {number|Object}         the tab size or the tabOverride object
731     */
732    tabOverride.tabSize = function (size) {
733        var i;
734
735        if (arguments.length) {
736            if (size && typeof size === 'number' && size > 0) {
737                aTab = '';
738                for (i = 0; i < size; i += 1) {
739                    aTab += ' ';
740                }
741            } else {
742                // size is falsy (0), not a number, or a negative number
743                aTab = '\t';
744            }
745            return this;
746        }
747
748        return (aTab === '\t') ? 0 : aTab.length;
749    };
750
751    /**
752     * Gets or sets the auto indent setting. True if each line should be
753     * automatically indented (default = true).
754     *
755     * @param  {boolean}        [enable]  whether auto indent should be enabled
756     * @return {boolean|Object}           whether auto indent is enabled or the
757     *                                    tabOverride object
758     */
759    tabOverride.autoIndent = function (enable) {
760        if (arguments.length) {
761            autoIndent = enable ? true : false;
762            return this;
763        }
764
765        return autoIndent;
766    };
767
768    /**
769     * Gets or sets the tab key combination.
770     *
771     * @param  {number}        keyCode             the key code of the key to use for tab
772     * @param  {string[]}      [modifierKeyNames]  the modifier key names - valid names are
773     *                                             'alt', 'ctrl', 'meta', and 'shift'
774     * @return {string|Object}                     the current tab key combination or the
775     *                                             tabOverride object
776     *
777     * @method
778     */
779    tabOverride.tabKey = createKeyComboFunction(function (keyCode) {
780        if (!arguments.length) {
781            return tabKey;
782        }
783        tabKey = keyCode;
784    }, tabModifierKeys);
785
786    /**
787     * Gets or sets the untab key combination.
788     *
789     * @param  {number}        keyCode             the key code of the key to use for untab
790     * @param  {string[]}      [modifierKeyNames]  the modifier key names - valid names are
791     *                                             'alt', 'ctrl', 'meta', and 'shift'
792     * @return {string|Object}                     the current untab key combination or the
793     *                                             tabOverride object
794     *
795     * @method
796     */
797    tabOverride.untabKey = createKeyComboFunction(function (keyCode) {
798        if (!arguments.length) {
799            return untabKey;
800        }
801        untabKey = keyCode;
802    }, untabModifierKeys);
803}));
804