274 lines
11 KiB
JavaScript
274 lines
11 KiB
JavaScript
10 years ago
|
/**
|
||
|
* @license Rangy Inputs, a jQuery plug-in for selection and caret manipulation within textareas and text inputs.
|
||
|
*
|
||
|
* https://github.com/timdown/rangyinputs
|
||
|
*
|
||
|
* For range and selection features for contenteditable, see Rangy.
|
||
|
|
||
|
* http://code.google.com/p/rangy/
|
||
|
*
|
||
|
* Depends on jQuery 1.0 or later.
|
||
|
*
|
||
|
* Copyright 2013, Tim Down
|
||
|
* Licensed under the MIT license.
|
||
|
* Version: 1.1.2
|
||
|
* Build date: 6 September 2013
|
||
|
*/
|
||
|
(function($) {
|
||
|
var UNDEF = "undefined";
|
||
|
var getSelection, setSelection, deleteSelectedText, deleteText, insertText;
|
||
|
var replaceSelectedText, surroundSelectedText, extractSelectedText, collapseSelection;
|
||
|
|
||
|
// 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.log) {
|
||
|
window.console.log("RangyInputs not supported in your browser. Reason: " + reason);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function adjustOffsets(el, start, end) {
|
||
|
if (start < 0) {
|
||
|
start += el.value.length;
|
||
|
}
|
||
|
if (typeof end == UNDEF) {
|
||
|
end = start;
|
||
|
}
|
||
|
if (end < 0) {
|
||
|
end += el.value.length;
|
||
|
}
|
||
|
return { start: start, end: end };
|
||
|
}
|
||
|
|
||
|
function makeSelection(el, start, end) {
|
||
|
return {
|
||
|
start: start,
|
||
|
end: end,
|
||
|
length: end - start,
|
||
|
text: el.value.slice(start, end)
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function getBody() {
|
||
|
return isHostObject(document, "body") ? document.body : document.getElementsByTagName("body")[0];
|
||
|
}
|
||
|
|
||
|
$(document).ready(function() {
|
||
|
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, 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, normalizedValue, textInputRange, len, endRange;
|
||
|
var range = document.selection.createRange();
|
||
|
|
||
|
if (range && range.parentElement() == el) {
|
||
|
len = el.value.length;
|
||
|
|
||
|
normalizedValue = el.value.replace(/\r\n/g, "\n");
|
||
|
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);
|
||
|
start += normalizedValue.slice(0, start).split("\n").length - 1;
|
||
|
if (textInputRange.compareEndPoints("EndToEnd", endRange) > -1) {
|
||
|
end = len;
|
||
|
} else {
|
||
|
end = -textInputRange.moveEnd("character", -len);
|
||
|
end += normalizedValue.slice(0, end).split("\n").length - 1;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return makeSelection(el, 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);
|
||
|
|
||
|
deleteText = function(el, start, end, moveSelection) {
|
||
|
var val;
|
||
|
if (start != end) {
|
||
|
val = el.value;
|
||
|
el.value = val.slice(0, start) + val.slice(end);
|
||
|
}
|
||
|
if (moveSelection) {
|
||
|
setSelection(el, start, start);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
deleteSelectedText = function(el) {
|
||
|
var sel = getSelection(el);
|
||
|
deleteText(el, sel.start, sel.end, true);
|
||
|
};
|
||
|
|
||
|
extractSelectedText = function(el) {
|
||
|
var sel = getSelection(el), val;
|
||
|
if (sel.start != sel.end) {
|
||
|
val = el.value;
|
||
|
el.value = val.slice(0, sel.start) + val.slice(sel.end);
|
||
|
}
|
||
|
setSelection(el, sel.start, sel.start);
|
||
|
return sel.text;
|
||
|
};
|
||
|
|
||
|
var updateSelectionAfterInsert = function(el, startIndex, text, selectionBehaviour) {
|
||
|
var endIndex = startIndex + text.length;
|
||
|
|
||
|
selectionBehaviour = (typeof selectionBehaviour == "string") ?
|
||
|
selectionBehaviour.toLowerCase() : "";
|
||
|
|
||
|
if ((selectionBehaviour == "collapsetoend" || selectionBehaviour == "select") && /[\r\n]/.test(text)) {
|
||
|
// Find the length of the actual text inserted, which could vary
|
||
|
// depending on how the browser deals with line breaks
|
||
|
var normalizedText = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
||
|
endIndex = startIndex + normalizedText.length;
|
||
|
var firstLineBreakIndex = startIndex + normalizedText.indexOf("\n");
|
||
|
|
||
|
if (el.value.slice(firstLineBreakIndex, firstLineBreakIndex + 2) == "\r\n") {
|
||
|
// Browser uses \r\n, so we need to account for extra \r characters
|
||
|
endIndex += normalizedText.match(/\n/g).length;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
switch (selectionBehaviour) {
|
||
|
case "collapsetostart":
|
||
|
setSelection(el, startIndex, startIndex);
|
||
|
break;
|
||
|
case "collapsetoend":
|
||
|
setSelection(el, endIndex, endIndex);
|
||
|
break;
|
||
|
case "select":
|
||
|
setSelection(el, startIndex, endIndex);
|
||
|
break;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
insertText = function(el, text, index, selectionBehaviour) {
|
||
|
var val = el.value;
|
||
|
el.value = val.slice(0, index) + text + val.slice(index);
|
||
|
if (typeof selectionBehaviour == "boolean") {
|
||
|
selectionBehaviour = selectionBehaviour ? "collapseToEnd" : "";
|
||
|
}
|
||
|
updateSelectionAfterInsert(el, index, text, selectionBehaviour);
|
||
|
};
|
||
|
|
||
|
replaceSelectedText = function(el, text, selectionBehaviour) {
|
||
|
var sel = getSelection(el), val = el.value;
|
||
|
el.value = val.slice(0, sel.start) + text + val.slice(sel.end);
|
||
|
updateSelectionAfterInsert(el, sel.start, text, selectionBehaviour || "collapseToEnd");
|
||
|
};
|
||
|
|
||
|
surroundSelectedText = function(el, before, after, selectionBehaviour) {
|
||
|
if (typeof after == UNDEF) {
|
||
|
after = before;
|
||
|
}
|
||
|
var sel = getSelection(el), val = el.value;
|
||
|
el.value = val.slice(0, sel.start) + before + sel.text + after + val.slice(sel.end);
|
||
|
var startIndex = sel.start + before.length;
|
||
|
updateSelectionAfterInsert(el, startIndex, sel.text, selectionBehaviour || "select");
|
||
|
};
|
||
|
|
||
|
function jQuerify(func, returnThis) {
|
||
|
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"))) {
|
||
|
var args = [el].concat(Array.prototype.slice.call(arguments));
|
||
|
var result = func.apply(this, args);
|
||
|
if (!returnThis) {
|
||
|
return result;
|
||
|
}
|
||
|
}
|
||
|
if (returnThis) {
|
||
|
return this;
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
|
||
|
$.fn.extend({
|
||
|
getSelection: jQuerify(getSelection, false),
|
||
|
setSelection: jQuerify(setSelection, true),
|
||
|
collapseSelection: jQuerify(collapseSelection, true),
|
||
|
deleteSelectedText: jQuerify(deleteSelectedText, true),
|
||
|
deleteText: jQuerify(deleteText, true),
|
||
|
extractSelectedText: jQuerify(extractSelectedText, false),
|
||
|
insertText: jQuerify(insertText, true),
|
||
|
replaceSelectedText: jQuerify(replaceSelectedText, true),
|
||
|
surroundSelectedText: jQuerify(surroundSelectedText, true)
|
||
|
});
|
||
|
});
|
||
|
})(jQuery);
|