/*! 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);
}));