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