Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/* valSel.js
 * ver. 2013-10
 *
 * A cross-browser textarea and text input value and selection manipulation
 * library plug-in for jQuery
 * Home: http://en.wikipedia.org/wiki/User:V111P/js/valSel
 *
 * This script contains code from Rangy Text Inputs (Version from 5 November 2010),
 * Copyright 2010, Tim Down, licensed under the MIT license.
 * http://code.google.com/p/rangyinputs/
 *
 * You can use the code not from Rangy Text Inputs under the CC0 license
 */

(function($) {
	var UNDEF = "undefined";
	var getSelection, setSelection, collapseSelection;
	var inited; // whether init() has already been called or not
	var rReG = /\r/g;

	// Trio of isHost* functions taken from Peter Michaux's article:
	// http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting
	function isHostMethod(object, property) {
		var t = typeof object[property];
		return t === "function" || (!!(t == "object" && object[property])) || t == "unknown";
	}

	function isHostProperty(object, property) {
		return typeof(object[property]) != UNDEF;
	}

	function isHostObject(object, property) {
		return !!(typeof(object[property]) == "object" && object[property]);
	}

	function fail(reason) {
		if (window.console && window.console.error) {
			window.console.error("valSel.js: Unsupported browser: " + reason);
		}
	}

	function adjustOffsets(el, start, end) {
		var len = el.value.replace(rReG, '').length;
		start = (start > 0 ? start : 0);
		start = (start < len ? start : len);
		if (typeof end == UNDEF)
			end = start;
		else {
			end = (end > 0 ? end : 0);
			end = (end < len ? end : len);
		}
		if (end < start) {
			var t = start;
			start = end;
			end = t;
		}
		return { start: start, end: end };
	}

	function makeSelection(normalizedValue, start, end) {
		return {
			start: start,
			end: end,
			length: end - start,
			text: normalizedValue.slice(start, end)
		};
	}

	function getBody() {
		return isHostObject(document, "body") ? document.body : document.getElementsByTagName("body")[0];
	}

	function init() {
		if (inited)
			return;
		var testTextArea = document.createElement("textarea");

		getBody().appendChild(testTextArea);

		if (isHostProperty(testTextArea, "selectionStart") && isHostProperty(testTextArea, "selectionEnd")) {
			getSelection = function(el) {
				var start = el.selectionStart, end = el.selectionEnd;
				return makeSelection(el.value.replace(rReG, ''), start, end);
			};

			setSelection = function(el, startOffset, endOffset) {
				var offsets = adjustOffsets(el, startOffset, endOffset);
				el.selectionStart = offsets.start;
				el.selectionEnd = offsets.end;
			};

			collapseSelection = function(el, toStart) {
				if (toStart) {
					el.selectionEnd = el.selectionStart;
				} else {
					el.selectionStart = el.selectionEnd;
				}
			};
		} else if (isHostMethod(testTextArea, "createTextRange") && isHostObject(document, "selection")
		           && isHostMethod(document.selection, "createRange")) {

			getSelection = function(el) {
				var start = 0, end = 0, range, normalizedValue = '', textInputRange, len, endRange;

				el.focus();
				range = document.selection.createRange();
				if (range && range.parentElement() == el) {
					len = el.value.length;
					normalizedValue = el.value.replace(rReG, "");
					textInputRange = el.createTextRange();
					textInputRange.moveToBookmark(range.getBookmark());
					endRange = el.createTextRange();
					endRange.collapse(false);
					if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) {
						start = end = len;
					} else {
						start = -textInputRange.moveStart("character", -len);
						if (textInputRange.compareEndPoints("EndToEnd", endRange) > -1) {
							end = len;
						} else {
							end = -textInputRange.moveEnd("character", -len);
						}
					}
				}

				return makeSelection(normalizedValue, start, end);
			};

			// Moving across a line break only counts as moving one character in a TextRange,
			// whereas a line break in the textarea value is two characters.
			// This function corrects for that by converting a text offset into a range character offset
			// by subtracting one character for every line break in the textarea prior to the offset
			var offsetToRangeCharacterMove = function(el, offset) {
				return offset;// - (el.value.slice(0, offset).split("\r\n").length - 1);
			};

			setSelection = function(el, startOffset, endOffset) {
				var offsets = adjustOffsets(el, startOffset, endOffset);
				var range = el.createTextRange();
				var startCharMove = offsetToRangeCharacterMove(el, offsets.start);
				range.collapse(true);
				if (offsets.start == offsets.end) {
					range.move("character", startCharMove);
				} else {
					range.moveEnd("character", offsetToRangeCharacterMove(el, offsets.end));
					range.moveStart("character", startCharMove);
				}
				range.select();
			};

			collapseSelection = function(el, toStart) {
				var range = document.selection.createRange();
				range.collapse(toStart);
				range.select();
			};
		} else {
			getBody().removeChild(testTextArea);
			fail("No means of finding text input caret position");
			return;
		}

		// Clean up
		getBody().removeChild(testTextArea);
		inited = true;
	} // init


	// the functions always automatically return this
	// if they don't otherwise return a result
	function jQuerify(func) {
		return function() {
			var el = this.jquery ? this[0] : this;
			var nodeName = el.nodeName.toLowerCase();

			if (el.nodeType == 1 && (nodeName == "textarea"
			    || (nodeName == "input" && el.type == "text"))) {
				if (!inited) init(); // because $(init) won't work after an error from another script
				var args = [el].concat(Array.prototype.slice.call(arguments));
				var result = func.apply(this, args);
				if (typeof result != 'undefined') {
					return result;
				}
			}
			return this;
		};
	}


	/* Rangy Text Inputs code ends */


	function getValue(el) {
		return el.value.replace(rReG, ''); // remove \r
	}


	function setValue(el, val) {
		var scrollPosObj = scrollPos(el);
		el.value = val;
		scrollPos(el, scrollPosObj);
	}


	function scrollPos(el, scrollPosObj) {
		if (scrollPosObj) {
			el.scrollTop = scrollPosObj.top;
			el.scrollLeft = scrollPosObj.left;
		}
		else {
			return {
				top: el.scrollTop,
				left: el.scrollLeft
			};
		}
	}


	var valParts = function (el, arg1, selText, textAfter) {
		var parts;
		var setValParts = function (el, parts) {
			setValue(el, parts.join(''));
			var beforeLen = parts[0].length;
			setSelection(el, beforeLen, beforeLen + parts[1].length);
		}

		if (typeof arg1 == 'object') {
			parts = arg1;
		}
		else if (typeof arg1 == 'function') {
			
		}
		else if (typeof arg1 == 'string') {
			parts = [arg1, selText, textAfter];
		}
		if (parts) {
			setValParts(el, parts);
			return;
		}
		else {
			var s = getSelection(el);
			var text = getValue(el);

			parts = [text.slice(0, s.start), s.text, text.slice(s.end)];

			if (typeof arg1 == 'function') {
				var parts = arg1(parts[0], parts[1], parts[2], text.length);
				if (parts)
					setValParts(el, parts);
				return;
			}

			return parts;
		}
	} // valParts


	var sel = function (el, arg1, arg2) {
		var sel;
		var setSel = function (sel) {
			if (typeof sel.start == 'number') {
				if (typeof sel.end != 'number')
					sel.end = sel.start;
				if (typeof sel.text == 'string')
					setValue(sel.text);
				setSelection(el, sel.start, sel.end);
			}
			else if (typeof sel.text == 'string') {
				valParts(el, function (pre, currSel, post) {
					var replacementSel = sel.text;
					if (collapse < 0) {
						post = replacementSel + post;
						replacementSel = '';
					}
					else if (collapse > 0) {
						post += replacementSel;
						replacementSel = '';
					}

					return [pre, replacementSel, post];
				});
			}
		}

		if (typeof arg1 == 'undefined') {
			return getSelection(el);
		}
		switch (typeof arg1) {
		case 'function':
			var selObj = getSelection(el);
			sel = arg1(selObj.text, selObj.start, selObj.end, selObj.length);
			if (typeof sel == 'string')
				sel = {text: sel};
			if (typeof sel == 'object')
				setSel(sel);
			break;
		case 'string':
			var collapse = (typeof arg2 == 'number' ? arg2 : 0);
			setSel({text: arg1, collapse: arg2});
			break;
		case 'number':
			var start = arg1, end = arg2;
			setSelection(el, start, end);
			break;
		}
	} // sel


	var aroundSel = function (el) {
		var a = arguments;
		var before = a[1];
		var after, include, collapse;
		var excludedBefore = '', excludedAfter = '';

		if (typeof before != 'string')
			return this; // error - do nothing
		if (typeof a[2] != 'string') {
			// aroundSel(string surround, [bool include, [int collapse]])
			after = before;
			include = a[2];
			collapse = a[3];
		}
		else if (typeof a[3] != 'string') {
			// aroundSel(string before, string after, [bool include, [int collapse]])
			after = a[2];
			include = a[3];
			collapse = a[4];
		}
		else {
			// aroundSel(string before, string prepend, string append, string after, [int collapse])
			excludedBefore = before;
			before = a[2];
			after = a[3];
			excludedAfter = a[4];
			collapse = a[5];
			include = true;
		}

		include = (typeof include == 'boolean' ? include : true);

		if (collapse === true)
			collapse = 1;
		else if (typeof collapse != 'number')
			collapse = 0;

		valParts(el, function (pre, sel, post) {
			if (include)
				sel = before + sel + after;
			else {
				pre = pre + before;
				post = after + post;
			}

			if (collapse < 0) {
				post = sel + post;
				sel = '';
			}
			else if (collapse > 0) {
				pre += sel;
				sel = '';
			}

			return [pre, sel, post];
		});
	} // aroundSel


	$.fn.extend({
		valParts: jQuerify(valParts),
		sel: jQuerify(sel),
		aroundSel: jQuerify(aroundSel),
		collapseSel: jQuerify(collapseSelection)
	});


	$(init);
})(jQuery);