/*! taboverride v4.0.2 | https://github.com/wjbryant/taboverride (c) 2014 Bill Bryant | http://opensource.org/licenses/mit */ /** * @fileOverview taboverride * @author Bill Bryant * @version 4.0.2 */ /*jslint browser: true */ /*global exports, define */ // use CommonJS or AMD if available (function (factory) { 'use strict'; var mod; if (typeof exports === 'object') { // Node.js/CommonJS factory(exports); } else if (typeof define === 'function' && define.amd) { // AMD - register as an anonymous module // files must be concatenated using an AMD-aware tool such as r.js define(['exports'], factory); } else { // no module format - create global variable mod = window.tabOverride = {}; factory(mod); } }(function (tabOverride) { 'use strict'; /** * The tabOverride namespace object * * @namespace tabOverride */ var document = window.document, listeners, aTab = '\t', // the string representing a tab tabKey = 9, untabKey = 9, tabModifierKeys = [], untabModifierKeys = ['shiftKey'], autoIndent = true, // whether each line should be automatically indented inWhitespace = false, // whether the start of the selection is in the leading whitespace on enter textareaElem = document.createElement('textarea'), // temp textarea element to get newline character(s) newline, // the newline character sequence (\n or \r\n) newlineLen, // the number of characters used for a newline (1 or 2) hooks = {}; /** * Determines whether the specified modifier keys match the modifier keys * that were pressed. * * @param {string[]} modifierKeys the modifier keys to check - ex: ['shiftKey'] * @param {Event} e the event object for the keydown event * @return {boolean} whether modifierKeys are valid for the event * * @method tabOverride.utils.isValidModifierKeyCombo */ function isValidModifierKeyCombo(modifierKeys, e) { var modifierKeyNames = ['alt', 'ctrl', 'meta', 'shift'], numModKeys = modifierKeys.length, i, j, currModifierKey, isValid = true; // check that all required modifier keys were pressed for (i = 0; i < numModKeys; i += 1) { if (!e[modifierKeys[i]]) { isValid = false; break; } } // if the requirements were met, check for additional modifier keys if (isValid) { for (i = 0; i < modifierKeyNames.length; i += 1) { currModifierKey = modifierKeyNames[i] + 'Key'; // if this key was pressed if (e[currModifierKey]) { // if there are required keys, check whether the current key // is required if (numModKeys) { isValid = false; // if this is a required key, continue for (j = 0; j < numModKeys; j += 1) { if (currModifierKey === modifierKeys[j]) { isValid = true; break; } } } else { // no required keys, but one was pressed isValid = false; } } // an extra key was pressed, don't check anymore if (!isValid) { break; } } } return isValid; } /** * Determines whether the tab key combination was pressed. * * @param {number} keyCode the key code of the key that was pressed * @param {Event} e the event object for the key event * @return {boolean} whether the tab key combo was pressed * * @private */ function tabKeyComboPressed(keyCode, e) { return keyCode === tabKey && isValidModifierKeyCombo(tabModifierKeys, e); } /** * Determines whether the untab key combination was pressed. * * @param {number} keyCode the key code of the key that was pressed * @param {Event} e the event object for the key event * @return {boolean} whether the untab key combo was pressed * * @private */ function untabKeyComboPressed(keyCode, e) { return keyCode === untabKey && isValidModifierKeyCombo(untabModifierKeys, e); } /** * Creates a function to get and set the specified key combination. * * @param {Function} keyFunc getter/setter function for the key * @param {string[]} modifierKeys the array of modifier keys to manipulate * @return {Function} a getter/setter function for the specified * key combination * * @private */ function createKeyComboFunction(keyFunc, modifierKeys) { return function (keyCode, modifierKeyNames) { var i, keyCombo = ''; if (arguments.length) { if (typeof keyCode === 'number') { keyFunc(keyCode); modifierKeys.length = 0; // clear the array if (modifierKeyNames && modifierKeyNames.length) { for (i = 0; i < modifierKeyNames.length; i += 1) { modifierKeys.push(modifierKeyNames[i] + 'Key'); } } } return this; } for (i = 0; i < modifierKeys.length; i += 1) { keyCombo += modifierKeys[i].slice(0, -3) + '+'; } return keyCombo + keyFunc(); }; } /** * Event handler to insert or remove tabs and newlines on the keydown event * for the tab or enter key. * * @param {Event} e the event object * * @method tabOverride.handlers.keydown */ function overrideKeyDown(e) { e = e || event; // textarea elements can only contain text nodes which don't receive // keydown events, so the event target/srcElement will always be the // textarea element, however, prefer currentTarget in order to support // delegated events in compliant browsers var target = e.currentTarget || e.srcElement, // don't use the "this" keyword (doesn't work in old IE) key = e.keyCode, // the key code for the key that was pressed tab, // the string representing a tab tabLen, // the length of a tab text, // initial text in the textarea range, // the IE TextRange object tempRange, // used to calculate selection start and end positions in IE preNewlines, // the number of newline character sequences before the selection start (for IE) selNewlines, // the number of newline character sequences within the selection (for IE) initScrollTop, // initial scrollTop value used to fix scrolling in Firefox selStart, // the selection start position selEnd, // the selection end position sel, // the selected text startLine, // for multi-line selections, the first character position of the first line endLine, // for multi-line selections, the last character position of the last line numTabs, // the number of tabs inserted / removed in the selection startTab, // if a tab was removed from the start of the first line preTab, // if a tab was removed before the start of the selection whitespace, // the whitespace at the beginning of the first selected line whitespaceLen, // the length of the whitespace at the beginning of the first selected line CHARACTER = 'character'; // string constant used for the Range.move methods // don't do any unnecessary work if ((target.nodeName && target.nodeName.toLowerCase() !== 'textarea') || (key !== tabKey && key !== untabKey && (key !== 13 || !autoIndent))) { return; } // initialize variables used for tab and enter keys inWhitespace = false; // this will be set to true if enter is pressed in the leading whitespace text = target.value; // this is really just for Firefox, but will be used by all browsers that support // selectionStart and selectionEnd - whenever the textarea value property is reset, // Firefox scrolls back to the top - this is used to set it back to the original value // scrollTop is nonstandard, but supported by all modern browsers initScrollTop = target.scrollTop; // get the text selection if (typeof target.selectionStart === 'number') { selStart = target.selectionStart; selEnd = target.selectionEnd; sel = text.slice(selStart, selEnd); } else if (document.selection) { // IE range = document.selection.createRange(); sel = range.text; tempRange = range.duplicate(); tempRange.moveToElementText(target); tempRange.setEndPoint('EndToEnd', range); selEnd = tempRange.text.length; selStart = selEnd - sel.length; // whenever the value of the textarea is changed, the range needs to be reset // IE <9 (and Opera) use both \r and \n for newlines - this adds an extra character // that needs to be accounted for when doing position calculations with ranges // these values are used to offset the selection start and end positions if (newlineLen > 1) { preNewlines = text.slice(0, selStart).split(newline).length - 1; selNewlines = sel.split(newline).length - 1; } else { preNewlines = selNewlines = 0; } } else { return; // cannot access textarea selection - do nothing } // tab / untab key - insert / remove tab if (key === tabKey || key === untabKey) { // initialize tab variables tab = aTab; tabLen = tab.length; numTabs = 0; startTab = 0; preTab = 0; // multi-line selection if (selStart !== selEnd && sel.indexOf('\n') !== -1) { // for multiple lines, only insert / remove tabs from the beginning of each line // find the start of the first selected line if (selStart === 0 || text.charAt(selStart - 1) === '\n') { // the selection starts at the beginning of a line startLine = selStart; } else { // the selection starts after the beginning of a line // set startLine to the beginning of the first partially selected line // subtract 1 from selStart in case the cursor is at the newline character, // for instance, if the very end of the previous line was selected // add 1 to get the next character after the newline // if there is none before the selection, lastIndexOf returns -1 // when 1 is added to that it becomes 0 and the first character is used startLine = text.lastIndexOf('\n', selStart - 1) + 1; } // find the end of the last selected line if (selEnd === text.length || text.charAt(selEnd) === '\n') { // the selection ends at the end of a line endLine = selEnd; } else if (text.charAt(selEnd - 1) === '\n') { // the selection ends at the start of a line, but no // characters are selected - don't indent this line endLine = selEnd - 1; } else { // the selection ends before the end of a line // set endLine to the end of the last partially selected line endLine = text.indexOf('\n', selEnd); if (endLine === -1) { endLine = text.length; } } // tab key combo - insert tabs if (tabKeyComboPressed(key, e)) { numTabs = 1; // for the first tab // insert tabs at the beginning of each line of the selection target.value = text.slice(0, startLine) + tab + text.slice(startLine, endLine).replace(/\n/g, function () { numTabs += 1; return '\n' + tab; }) + text.slice(endLine); // set start and end points if (range) { // IE range.collapse(); range.moveEnd(CHARACTER, selEnd + (numTabs * tabLen) - selNewlines - preNewlines); range.moveStart(CHARACTER, selStart + tabLen - preNewlines); range.select(); } else { // the selection start is always moved by 1 character target.selectionStart = selStart + tabLen; // move the selection end over by the total number of tabs inserted target.selectionEnd = selEnd + (numTabs * tabLen); target.scrollTop = initScrollTop; } } else if (untabKeyComboPressed(key, e)) { // if the untab key combo was pressed, remove tabs instead of inserting them if (text.slice(startLine).indexOf(tab) === 0) { // is this tab part of the selection? if (startLine === selStart) { // it is, remove it sel = sel.slice(tabLen); } else { // the tab comes before the selection preTab = tabLen; } startTab = tabLen; } target.value = text.slice(0, startLine) + text.slice(startLine + preTab, selStart) + sel.replace(new RegExp('\n' + tab, 'g'), function () { numTabs += 1; return '\n'; }) + text.slice(selEnd); // set start and end points if (range) { // IE // setting end first makes calculations easier range.collapse(); range.moveEnd(CHARACTER, selEnd - startTab - (numTabs * tabLen) - selNewlines - preNewlines); range.moveStart(CHARACTER, selStart - preTab - preNewlines); range.select(); } else { // set start first for Opera target.selectionStart = selStart - preTab; // preTab is 0 or tabLen // move the selection end over by the total number of tabs removed target.selectionEnd = selEnd - startTab - (numTabs * tabLen); } } else { return; // do nothing for invalid key combinations } } else { // single line selection // tab key combo - insert a tab if (tabKeyComboPressed(key, e)) { if (range) { // IE range.text = tab; range.select(); } else { target.value = text.slice(0, selStart) + tab + text.slice(selEnd); target.selectionEnd = target.selectionStart = selStart + tabLen; target.scrollTop = initScrollTop; } } else if (untabKeyComboPressed(key, e)) { // if the untab key combo was pressed, remove a tab instead of inserting one // if the character before the selection is a tab, remove it if (text.slice(selStart - tabLen).indexOf(tab) === 0) { target.value = text.slice(0, selStart - tabLen) + text.slice(selStart); // set start and end points if (range) { // IE // collapses range and moves it by -1 tab range.move(CHARACTER, selStart - tabLen - preNewlines); range.select(); } else { target.selectionEnd = target.selectionStart = selStart - tabLen; target.scrollTop = initScrollTop; } } } else { return; // do nothing for invalid key combinations } } } else if (autoIndent) { // Enter key // insert a newline and copy the whitespace from the beginning of the line // find the start of the first selected line if (selStart === 0 || text.charAt(selStart - 1) === '\n') { // the selection starts at the beginning of a line // do nothing special inWhitespace = true; return; } // see explanation under "multi-line selection" above startLine = text.lastIndexOf('\n', selStart - 1) + 1; // find the end of the first selected line endLine = text.indexOf('\n', selStart); // if no newline is found, set endLine to the end of the text if (endLine === -1) { endLine = text.length; } // get the whitespace at the beginning of the first selected line (spaces and tabs only) whitespace = text.slice(startLine, endLine).match(/^[ \t]*/)[0]; whitespaceLen = whitespace.length; // the cursor (selStart) is in the whitespace at beginning of the line // do nothing special if (selStart < startLine + whitespaceLen) { inWhitespace = true; return; } if (range) { // IE // insert the newline and whitespace range.text = '\n' + whitespace; range.select(); } else { // insert the newline and whitespace target.value = text.slice(0, selStart) + '\n' + whitespace + text.slice(selEnd); // Opera uses \r\n for a newline, instead of \n, // so use newlineLen instead of a hard-coded value target.selectionEnd = target.selectionStart = selStart + newlineLen + whitespaceLen; target.scrollTop = initScrollTop; } } if (e.preventDefault) { e.preventDefault(); } else { e.returnValue = false; return false; } } /** * Event handler to prevent the default action for the keypress event when * tab or enter is pressed. Opera and Firefox also fire a keypress event * when the tab or enter key is pressed. Opera requires that the default * action be prevented on this event or the textarea will lose focus. * * @param {Event} e the event object * * @method tabOverride.handlers.keypress */ function overrideKeyPress(e) { e = e || event; var key = e.keyCode; if (tabKeyComboPressed(key, e) || untabKeyComboPressed(key, e) || (key === 13 && autoIndent && !inWhitespace)) { if (e.preventDefault) { e.preventDefault(); } else { e.returnValue = false; return false; } } } /** * Executes all registered extension functions for the specified hook. * * @param {string} hook the name of the hook for which the extensions are registered * @param {Array} [args] the arguments to pass to the extension * * @method tabOverride.utils.executeExtensions */ function executeExtensions(hook, args) { var i, extensions = hooks[hook] || [], len = extensions.length; for (i = 0; i < len; i += 1) { extensions[i].apply(null, args); } } /** * @typedef {Object} tabOverride.utils~handlerObj * * @property {string} type the event type * @property {Function} handler the handler function - passed an Event object */ /** * @typedef {Object} tabOverride.utils~listenersObj * * @property {Function} add Adds all the event listeners to the * specified element * @property {Function} remove Removes all the event listeners from * the specified element */ /** * Creates functions to add and remove event listeners in a cross-browser * compatible way. * * @param {tabOverride.utils~handlerObj[]} handlerList an array of {@link tabOverride.utils~handlerObj handlerObj} objects * @return {tabOverride.utils~listenersObj} a listenersObj object used to add and remove the event listeners * * @method tabOverride.utils.createListeners */ function createListeners(handlerList) { var i, len = handlerList.length, remove, add; function loop(func) { for (i = 0; i < len; i += 1) { func(handlerList[i].type, handlerList[i].handler); } } // use the standard event handler registration method when available if (document.addEventListener) { remove = function (elem) { loop(function (type, handler) { elem.removeEventListener(type, handler, false); }); }; add = function (elem) { // remove listeners before adding them to make sure they are not // added more than once remove(elem); loop(function (type, handler) { elem.addEventListener(type, handler, false); }); }; } else if (document.attachEvent) { // support IE 6-8 remove = function (elem) { loop(function (type, handler) { elem.detachEvent('on' + type, handler); }); }; add = function (elem) { remove(elem); loop(function (type, handler) { elem.attachEvent('on' + type, handler); }); }; } return { add: add, remove: remove }; } /** * Adds the Tab Override event listeners to the specified element. * * Hooks: addListeners - passed the element to which the listeners will * be added. * * @param {Element} elem the element to which the listeners will be added * * @method tabOverride.utils.addListeners */ function addListeners(elem) { executeExtensions('addListeners', [elem]); listeners.add(elem); } /** * Removes the Tab Override event listeners from the specified element. * * Hooks: removeListeners - passed the element from which the listeners * will be removed. * * @param {Element} elem the element from which the listeners will be removed * * @method tabOverride.utils.removeListeners */ function removeListeners(elem) { executeExtensions('removeListeners', [elem]); listeners.remove(elem); } // Initialize Variables listeners = createListeners([ { type: 'keydown', handler: overrideKeyDown }, { type: 'keypress', handler: overrideKeyPress } ]); // get the characters used for a newline textareaElem.value = '\n'; newline = textareaElem.value; newlineLen = newline.length; textareaElem = null; // Public Properties and Methods /** * Namespace for utility methods * * @namespace */ tabOverride.utils = { executeExtensions: executeExtensions, isValidModifierKeyCombo: isValidModifierKeyCombo, createListeners: createListeners, addListeners: addListeners, removeListeners: removeListeners }; /** * Namespace for event handler functions * * @namespace */ tabOverride.handlers = { keydown: overrideKeyDown, keypress: overrideKeyPress }; /** * Adds an extension function to be executed when the specified hook is * "fired." The extension function is called for each element and is passed * any relevant arguments for the hook. * * @param {string} hook the name of the hook for which the extension * will be registered * @param {Function} func the function to be executed when the hook is "fired" * @return {Object} the tabOverride object */ tabOverride.addExtension = function (hook, func) { if (hook && typeof hook === 'string' && typeof func === 'function') { if (!hooks[hook]) { hooks[hook] = []; } hooks[hook].push(func); } return this; }; /** * Enables or disables Tab Override for the specified textarea element(s). * * Hooks: set - passed the current element and a boolean indicating whether * Tab Override was enabled or disabled. * * @param {Element|Element[]} elems the textarea element(s) for * which to enable or disable * Tab Override * @param {boolean} [enable=true] whether Tab Override should be * enabled for the element(s) * @return {Object} the tabOverride object */ tabOverride.set = function (elems, enable) { var enableFlag, elemsArr, numElems, setListeners, attrValue, i, elem; if (elems) { enableFlag = arguments.length < 2 || enable; // don't manipulate param when referencing arguments object // this is just a matter of practice elemsArr = elems; numElems = elemsArr.length; if (typeof numElems !== 'number') { elemsArr = [elemsArr]; numElems = 1; } if (enableFlag) { setListeners = addListeners; attrValue = 'true'; } else { setListeners = removeListeners; attrValue = ''; } for (i = 0; i < numElems; i += 1) { elem = elemsArr[i]; if (elem && elem.nodeName && elem.nodeName.toLowerCase() === 'textarea') { executeExtensions('set', [elem, enableFlag]); elem.setAttribute('data-taboverride-enabled', attrValue); setListeners(elem); } } } return this; }; /** * Gets or sets the tab size for all elements that have Tab Override enabled. * 0 represents the tab character. * * @param {number} [size] the tab size * @return {number|Object} the tab size or the tabOverride object */ tabOverride.tabSize = function (size) { var i; if (arguments.length) { if (size && typeof size === 'number' && size > 0) { aTab = ''; for (i = 0; i < size; i += 1) { aTab += ' '; } } else { // size is falsy (0), not a number, or a negative number aTab = '\t'; } return this; } return (aTab === '\t') ? 0 : aTab.length; }; /** * Gets or sets the auto indent setting. True if each line should be * automatically indented (default = true). * * @param {boolean} [enable] whether auto indent should be enabled * @return {boolean|Object} whether auto indent is enabled or the * tabOverride object */ tabOverride.autoIndent = function (enable) { if (arguments.length) { autoIndent = enable ? true : false; return this; } return autoIndent; }; /** * Gets or sets the tab key combination. * * @param {number} keyCode the key code of the key to use for tab * @param {string[]} [modifierKeyNames] the modifier key names - valid names are * 'alt', 'ctrl', 'meta', and 'shift' * @return {string|Object} the current tab key combination or the * tabOverride object * * @method */ tabOverride.tabKey = createKeyComboFunction(function (keyCode) { if (!arguments.length) { return tabKey; } tabKey = keyCode; }, tabModifierKeys); /** * Gets or sets the untab key combination. * * @param {number} keyCode the key code of the key to use for untab * @param {string[]} [modifierKeyNames] the modifier key names - valid names are * 'alt', 'ctrl', 'meta', and 'shift' * @return {string|Object} the current untab key combination or the * tabOverride object * * @method */ tabOverride.untabKey = createKeyComboFunction(function (keyCode) { if (!arguments.length) { return untabKey; } untabKey = keyCode; }, untabModifierKeys); }));