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