Have a problem with moveToPoint() method of textRange IE11;
Seems like it dosen't work if pointed node wasn't in first screen;
document.addEventListener( "click", function(e) {
var x = e.clientX;
var y = e.clientY;
var range = document.body.createTextRange();
range.moveToPoint(x, y);
range.expand('word');
console.log(range);
console.log(range.text);
});
This code grab words from click point, but it wokrs normal only if we clicking in node's that were on first scroll.
If we scroll little bit down to the node that wasnt in first scroll, we will catch the exeception.
Does anybody know how to handle this situation correctly?
You can use offsetX, offsetY properties.
Or you can add scroll position to x and y vars, using scrollLeft and scrollTop properties of parent element.
I can confirm that such a bug exists in IE11. You can find details here in comments: http://generatedcontent.org/post/69213745095/ie11review-part1
Possible solution (after you create a range):
range.moveToElementText(e.target);
range.collapse(true)
range.expand("word")
Now you have the first word selected. You have to check now if the selected word fits your mouse click position using TextRange properties boundingHeight, boundingWidth, boundingLeft and boundingTop. If it doesn't, you move in cycle to the next word:
range.collapse(false);
range.expand("word");
Well, inspired by #dinalt and #JAYBEkster, I came up this solution. Maybe someone will need this after all.
Code below(for IE, didn't check all versons, but it works fine 11+) grab word in complex nested html.
Step bys step how it works:
Firstable we create range from e.target
We collapse it to the begining.
That will be awesome if we just could expand("word") and iterate every each word, but unfortunantly we cant. So we loop through characters split them to words and compare their bounding's with e.clientX, and Y, in the other hand by iterating charakters insted of words we get more control.
We get correct one and split target.innerText with it, to get left and right part of node's text.
After that we split each part again but this our separators (regexp with all white spaces and word separaters i could imagine) and we get two arr's.
Why we do steps 4 and 5? Cause we find some text that end's in that node, but it may not be the end or start of ther word, for some styling reasons, acent part of ther word my bein it's own node and target.node my just be the middle part of the word. So we have to jump up throug node's and find word's ending.
If u sure that ur html dosen't complain that u can skip all other steps.
Then if arr is empty we run our recursive function that runs throug node's and grab's text until find separator.
Yeah it really look like roket since for such, that we mayed think simple task.
But I actualy couldn't find a better solution, There are was a couple of options, bu all of them wasnt's as universal as I need it to be.
Benefits of this code that it really dosen't care about complexivity of html at all.
I will post here the link to my github repositiry where u can find the full code of wordGraber, that will work this Chrome, Safari, FF and IE of course.
https://github.com/6graNik/wordGrabber
Below is just a part for IE.
function getWordFromEventIE(e) {
const x = e.clientX;
const y = e.clientY;
const innerText = e.target && e.target.innerText;
const separators = /([\s&^:;,!?(){}])+/;
const IErange = global.document.body.createTextRange();
try {
IErange.moveToElementText(e.target);
IErange.collapse(true);
let wholeSentenceLength = 0;
const reqIEcharTest = () => {
do {
IErange.expand("character");
wholeSentenceLength += 1;
}
while (!separators.test(IErange.text.slice(-1)) && wholeSentenceLength <= innerText.length);
const {boundingLeft, boundingTop, boundingWidth, boundingHeight, text} = IErange;
if (boundingLeft <= x && x <= (boundingLeft + boundingWidth)
&& boundingTop <= y && y <= (boundingTop + boundingHeight)) {
if (wholeSentenceLength <= innerText.length && !separators.test(text.slice(-1)) ) {
return text;
}
return text.substr(0, text.length - 1);
}
IErange.collapse(false);
return reqIEcharTest();
};
const text = reqIEcharTest().trim();
const innerTextArr = innerText.split(text);
const innerTextLeft = innerTextArr[0].split(separators);
const innerTextRight = innerTextArr[1].split(separators);
let leftPart;
if (innerTextLeft <= 1) {
leftPart = recursionWordGet(e.target, 'left') + innerTextLeft.slice(-1)[0];
} else {
leftPart = innerTextLeft.slice(-1)[0];
}
let rightPart;
if (innerTextRight <= 1) {
rightPart = innerTextRight[0] + recursionWordGet(e.target, 'right');
} else {
rightPart = innerTextRight[0];
}
return leftPart + text + rightPart;
} catch (err) {
console.log('>>>>>>>>>>>>>>>>>> text', err);
}
}
function recursionWordGet(target, option) {
const separators = /([\s&^:;,!?(){}])+/;
const uniqString = Date.now();
target.setAttribute("data-target", uniqString);
const {parentNode} = target;
const copyNode = parentNode.cloneNode(true);
copyNode.querySelector(`[data-target="${uniqString}"]`).innerText = uniqString;
const tagName = copyNode.tagName;
const text = copyNode.innerText;
const textArr = text.split(uniqString);
const textLeftPartArr = textArr[0].split(separators);
const textRightPartArr = textArr[1].split(separators);
if (option === 'right') {
let returnText;
if (textRightPartArr.length <= 1 && tagName === 'span') {
returnText = textRightPartArr[0] + recursionWordGet(parentNode, 'right');
} else {
returnText = textRightPartArr[0];
}
return returnText;
}
if (option === 'left') {
let returnText;
if (textLeftPartArr <= 1 && tagName === 'span') {
returnText = recursionWordGet(parentNode, 'left') + textLeftPartArr.slice(-1)[0];
} else {
returnText = textLeftPartArr.slice(-1)[0];
}
return returnText;
}
return '';
}
Related
I have a simple text area with text like this:
Lorem Ipsum [text] dolar, rock n'[more] roller.
I am trying to detect when my cursor is between the brackets and if so, allow for a Ctrl+ Right or left arrow to move the text and brackets right or left by one position with each key press without going past start or end of the line and without moving past an adjacent bracketed block. I guess this is the same as the Ctrl+Right arrow copying the first character (or space) on the right side, to the left side and vice versa.
I have some basic jquery skills but all of my attempts at this have failed miserably. Otherwise, I would have pasted in a partial block of code showing what I have. Thanks,
The solution will require a few different pieces:
Finding the cursor location:
function getCaretPosition(element)
{
var CaretPos = 0;
//Old IE way
if ( document.selection )
{
element.focus();
var textSelection = document.selection.createRange();
textSelection.moveStart( 'character', -element.value.length );
CaretPos = textSelection.text.length;
}
//DOM way
else if ( element.selectionStart || element.selectionStart == '0' )
CaretPos = element.selectionStart;
return (CaretPos);
}
Find the location of the string (to see if caret is inside it). You can use a Regular expression like:
var search = /(\[[^\[\]]+\])/g;
//Then loop and find all the matches and check if
//the caret is between them
var matches = myString.match( search );
You will also need to listen to keypress events on the textarea and in that event listener:
If it's a left arrow or right arrow (with ctrl button held down) then:
find caret position, see if it's inside a bracket piece of text and if so:
Move the text left or right (you'd grab substrings of the text before/after the bracketed text and create a new concatenated string)
Those pieces should make this work. (I'm not going to write it for you as that won't really help you)
Here is a fiddle that does this, and here is the code:
$('textarea').on('keydown', function(e) {
// Is Ctrl-left and Ctrl+right pressed?
if (e.ctrlKey && (e.which === 37 || e.which === 39)) {
var pos = this.selectionStart;
var val = this.value;
var openBracketOnLeft = val.substr(0, pos).lastIndexOf('[');
var closeBracketOnLeft = val.substr(0, pos).lastIndexOf(']');
var closeBracketOnRight = val.substr(pos).indexOf(']');
// Is start of selection within two brackets?
if (openBracketOnLeft > closeBracketOnLeft && closeBracketOnRight !== -1) {
closeBracketOnRight += pos + 1;
var tagText = val.substr(openBracketOnLeft, closeBracketOnRight - openBracketOnLeft);
var level = 0;
// Repeat moving the tag until we do not break another tag in two.
do {
// Is Ctrl-left pressed, and is tag not yet on far left?
if (e.which === 37 && openBracketOnLeft) {
ch = val.substr(openBracketOnLeft - 1, 1);
val = val.substr(0, openBracketOnLeft - 1)
+ tagText
+ ch
+ val.substr(closeBracketOnRight);
openBracketOnLeft--;
closeBracketOnRight--;
// Is Ctrl-right pressed, and is tag not yet on far right?
} else if (e.which === 39 && closeBracketOnRight < val.length) {
ch = val.substr(closeBracketOnRight, 1);
val = val.substr(0, openBracketOnLeft)
+ ch
+ tagText
+ val.substr(closeBracketOnRight + 1);
openBracketOnLeft++;
closeBracketOnRight++;
} else {
break;
}
level += ch == '[' ? 1 : ch == ']' ? -1 : 0;
} while (level);
// Select the tag, without the brackets
this.value = val;
this.selectionStart = openBracketOnLeft + 1;
this.selectionEnd = closeBracketOnRight - 1;
e.preventDefault();
}
};
});
Here is a way to do it without evil regex strings. Instead I wanted to try and do it with jQuery 'keydown' event which was inline with what the questioner mentioned (see: newb at jQuery). Also note that 'keydown' is better for this methodology as 'keyup' will fire multiple times, though I guess this will too... Anyways, here is what I came up with:
$('#inputFieldInQuestion').on('keydown', function (event) {
// if both the control key and left key are pushed
if (event.keyCode == 37 && event.ctrlKey) {
// grab the text from the input and caret position in the input box
var inputBoxText = $(this).val(),
currentCaretPosition = this.selectionStart
// loop through all the characters in the input box text
for (var i = 0; i < inputBoxText.length; i++) {
// if the current character is an open bracket start testing for the end
if (inputBoxText[i] === "[") {
for (var j = i + 1; j < inputBoxText.length; j++) {
// this means that there is another bracketed string in between the
// beginning and the current bracketed string
if (inputBoxText[j] === "[") { break }
// if instead we come to the end of the bracketed string will determine
// if the bounds make sense
else if (inputBoxText[j] === "]") {
// if the caret position is in the bounds that you have just created
// we continue the shift
if (currentCaretPosition > i && currentCaretPosition < j) {
// test as per the question if the bracketed string is adjascent
// to another bracketed string
if (inputBoxText[i - 1] !== "]") {
// if the bracketed text is all the way to the left of the
// input box
if (i > 0) {
// slice and dice the string and move things left by one
// character
var frontString = inputBoxText.substring(0, i),
stringToMove = inputBoxText.substring(i, j + 1),
endString = inputBoxText.substring(j + 1)
$(this).val(frontString.slice(0, i - 1) + stringToMove + frontString.slice(i - 1) + endString)
this.setSelectionRange(currentCaretPosition - 1, currentCaretPosition - 1); break
}
}
else { break }
}
}
}
}
}
// important so that the ctrl-left doesn't shift the cursor to the end of the word
return false;
}
// if both the control key and right key are pushed
else if (event.keyCode == 39 && event.ctrlKey) {
var inputBoxText = $(this).val(),
currentCaretPosition = this.selectionStart
for (var i = 0; i < inputBoxText.length; i++) {
if (inputBoxText[i] === "[") {
for (var j = i; j < inputBoxText.length; j++) {
if (inputBoxText[j] === "]") {
if (currentCaretPosition > i && currentCaretPosition < j) {
// test as per the question if the bracketed string is adjascent
// to another bracketed string
if (inputBoxText[j + 1] !== "[") {
// bracketed text is all the way to the right of the input box
if (inputBoxText.length - j > 1) {
var frontString = inputBoxText.substring(0, i),
stringToMove = inputBoxText.substring(i, j + 1),
endString = inputBoxText.substring(j + 1)
$(this).val(frontString + endString.slice(0, 1) + stringToMove + endString.slice(1))
this.setSelectionRange(currentCaretPosition + 1, currentCaretPosition + 1); break
}
}
else { break }
}
}
}
}
}
return false;
}
})
This might be the most complicated way to do this ever but it does seem to work and satisfy all the constraints posed. Since I just noticed that this was flagged regex, this might be a terrible solution. Let the evisceration begin!
Super Bonus: This will work if you have any number of "[]" pairs in the string.
I'm working on a chrome translate extension that when holding the ctrl key for seconds,the extension get the word under the cursor,translate it,and then display the result on the top of the word.
When dealing with getting the word under the cursor,I first need to create the range of the word under the cursor.I use the following code snippet to achieve this.I reference to here.https://stackoverflow.com/a/3710561/4244369
var getRangeAtPoint = function(elem, x, y) {
if (elem.nodeType == elem.TEXT_NODE) {
var range = elem.ownerDocument.createRange();
range.selectNodeContents(elem);
var currentPos = 0;
var endPos = range.endOffset;
while (currentPos + 1 < endPos) {
range.setStart(elem, currentPos);
range.setEnd(elem, currentPos + 1);
var range_rect = range.getBoundingClientRect();
if (range_rect.left <= x && range_rect.right >= x &&
range_rect.top <= y && range_rect.bottom >= y) {
range.expand("word");
return range;
}
currentPos += 1;
}
} else {
for (var i = 0; i < elem.childNodes.length; i++) {
var range = elem.childNodes[i].ownerDocument.createRange();
range.selectNodeContents(elem.childNodes[i]);
var range_rect = range.getBoundingClientRect();
if (range_rect.left <= x && range_rect.right >= x &&
range_rect.top <= y && range_rect.bottom >= y) {
range.detach();
var computed_range = getRangeAtPoint(elem.childNodes[i], x, y);
if(computed_range){
return computed_range;
}
} else {
range.detach();
}
}
}
return (null);
};
After creating the range,I can use range.toString() to get the word and range.getBoundingClientRect() to decide the position to display the result.It works well until I met the following case:
<p>click the <a href='#'>sample words</a> here</p>
If the cursor is under the word "words",it works properly.However,when the cursor is under the word "sample",after calling range.expand('word'),the client rect is wrong,the width of client rect should be the width of "sample",however,it's the width of "sample words".
I also include a jsfiddle here.https://jsfiddle.net/sangelee/1maqmm89/
Is it the problem of range.expand('word')?How to fix it?Or instead of using range.expand('word'),are there any way to achieve this?Any help is appreciated!ps.I'm using chrome 39.
The issue is with range.expand(), as you suspected. Also, the code to get the caret position as a range can be vastly simplified. Example (WebKit only):
https://jsfiddle.net/e5knrLv8/
The console also reveals that range.expand(), which has always been a WebKit-only non-standard method, has been deprecated in favour of Selection.modify(), which unfortunately is a bit of a pain to use in practice. Here is a revised example using Selection.modify, which does fix your issue:
https://jsfiddle.net/e5knrLv8/1/
I use a dhtml (midas) editor as a html editor in my web application, what I want to do is get a focused cursor in this html editor follow the mouse, is there a way to do that?
Added Example:
I want cursor in textarea follow the mouse so if you have a big text in your textarea and you are going over it with mouse, cursor (text cursor) should follow the mouse, like this:
"This is an ex|ample text" - if mouse is over "example" word and between x and a, text cursor (|) should be focused there but when I move mouse on for example "text" cursor | should be between letters where mouse is currently located.
Ok I found the solution using Ext.util.TextMetrics, first I get position of every character in editor, then I compare that to mouse cursor position and then update midas selection based on given character from charNum array
htmlEditor.getEl().on('mousemove', function(e)
{
var charNum = {},
text = htmlEditor.getValue(),
fWidth = htmlEditor.getWidth();
var textMetrics = new Ext.util.TextMetrics(htmlEditor.getEl(), htmlEditor.getWidth());
for(var n=0;n<text.length;n++)
{
var dat = text.substring(0, n)
var width = textMetrics.getWidth(dat);
var height = textMetrics.getHeight(dat);
if(width > fWidth)
{
var mult = Math.ceil(width/fWidth)
var width = width % fWidth;
height = height*mult;
}
charNum[n] = [width, height];
}
//console.log("charNum: "+charNum.toSource());
var mX = e.getX();
var mY = e.getY();
var cXY = htmlEditor.getEl().getXY();
var cX = cXY[0];
var cY = cXY[1];
var x = mX-cX-20;
var y = mY-cY;
//console.log("fin xy: "+x+' '+y);
var n = -1;
var z = 0;
for(key in charNum)
{
if(charNum[key][0] > x && charNum[key][1] > y)
{
n = key-1;
break;
}
n++;
z++;
}
if(x < 0 && y < 14) n = -1;
if(n == (z-1) && n != -1)
{
n++;
}
var selection = htmlEditor.win.getSelection();
range = selection.getRangeAt(0);
range.selectNodeContents(htmlEditor.getEditorBody());
range.collapse(true);
for(var x=0;x<n;x++)
{
selection.modify("move", "forward", "character");
}
});
Try Activates ExtJs HtmlEditor textarea when it is loaded and Sencha Docs, the official docs says:
Note: The focus/blur and validation marking functionality inherited from Ext.form.Field is NOT
supported by this editor.
I haven't tried the solution of #dfilkovi, but albeit it can be correct, bear in mind that any solution binding an event to mousemove will mostly certain cause a huge overhead on cpu.
To alleviate this symptom, you could unbind the listener first thing at the handler and then set a timeout to bind it after a few millisecs; something like:
// assume HandleOriginal as the original function declared by #dfilkovi
// attach the listener
startListener();
// functions
function startListener() {
htmlEditor.getEl().on('mousemove', HandleAndWait);
}
function stopListener() {
// maybe this is not the right syntax
htmlEditor.getEl().on('mousemove', null);
}
function HandleAndWait(e) {
var C_SLEEP = 50;
stopListener();
try { HandleOriginal(e); }
finally { window.setTimeout(startListener, C_SLEEP); }
}
You can, then, fine tune the value of C_SLEEP to the best user experience.
I would like to highlight (apply css to) a certain text range, denoted by its start and end position. This is more diffucult than it seems, since there may be other tags within the text, that need to be ignored.
Example:
<div>abcd<em>efg</em>hij</div>
highlight(2, 6) needs to highlight "cdef" without removing the tag.
I have tried already using a TextRange object, but without success.
Thanks in advance!
Below is a function to set the selection to a pair of character offsets within a particular element. This is naive implementation: it does not take into account any text that may be made invisible (either by CSS or by being inside a <script> or <style> element, for example) and may have browser discrepancies (IE versus everything else) with line breaks, and takes no account of collapsed whitespace (such as 2 or more consecutive space characters collapsing to one visible space on the page). However, it does work for your example in all major browsers.
For the other part, the highlighting, I'd suggest using document.execCommand() for that. You can use my function below to set the selection and then call document.execCommand(). You'll need to make the document temporarily editable in non-IE browsers for the command to work. See my answer here for code: getSelection & surroundContents across multiple tags
Here's a jsFiddle example showing the whole thing, working in all major browsers: http://jsfiddle.net/8mdX4/1211/
And the selection setting code:
function getTextNodesIn(node) {
var textNodes = [];
if (node.nodeType == 3) {
textNodes.push(node);
} else {
var children = node.childNodes;
for (var i = 0, len = children.length; i < len; ++i) {
textNodes.push.apply(textNodes, getTextNodesIn(children[i]));
}
}
return textNodes;
}
function setSelectionRange(el, start, end) {
if (document.createRange && window.getSelection) {
var range = document.createRange();
range.selectNodeContents(el);
var textNodes = getTextNodesIn(el);
var foundStart = false;
var charCount = 0, endCharCount;
for (var i = 0, textNode; textNode = textNodes[i++]; ) {
endCharCount = charCount + textNode.length;
if (!foundStart && start >= charCount
&& (start < endCharCount ||
(start == endCharCount && i <= textNodes.length))) {
range.setStart(textNode, start - charCount);
foundStart = true;
}
if (foundStart && end <= endCharCount) {
range.setEnd(textNode, end - charCount);
break;
}
charCount = endCharCount;
}
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
} else if (document.selection && document.body.createTextRange) {
var textRange = document.body.createTextRange();
textRange.moveToElementText(el);
textRange.collapse(true);
textRange.moveEnd("character", end);
textRange.moveStart("character", start);
textRange.select();
}
}
You could take a look at how works this powerful JavaScript utility which support selection over multiple DOM elements:
MASHA (short for Mark & Share) allow you to mark interesting parts of web page content and share it
http://mashajs.com/index_eng.html
It's also on GitHub https://github.com/SmartTeleMax/MaSha
Works even on Mobile Safari and IE!
Following solution doesn't work for IE, you'll need to apply TextRange objects etc. for that. As this uses selections to perform this, it shouldn't break the HTML in normal cases, for example:
<div>abcd<span>efg</span>hij</div>
With highlight(3,6);
outputs:
<div>abc<em>d<span>ef</span></em><span>g</span>hij</div>
Take note how it wraps the first character outside of the span into an em, and then the rest within the span into a new one. Where as if it would just open it at character 3 and end at character 6, it would give invalid markup like:
<div>abc<em>d<span>ef</em>g</span>hij</div>
The code:
var r = document.createRange();
var s = window.getSelection()
r.selectNode($('div')[0]);
s.removeAllRanges();
s.addRange(r);
// not quite sure why firefox has problems with this
if ($.browser.webkit) {
s.modify("move", "backward", "documentboundary");
}
function highlight(start,end){
for(var st=0;st<start;st++){
s.modify("move", "forward", "character");
}
for(var st=0;st<(end-start);st++){
s.modify("extend", "forward", "character");
}
}
highlight(2,6);
var ra = s.getRangeAt(0);
var newNode = document.createElement("em");
newNode.appendChild(ra.extractContents());
ra.insertNode(newNode);
Example: http://jsfiddle.net/niklasvh/4NDb9/
edit Looks like at least my FF4 had some issues with
s.modify("move", "backward", "documentboundary");
but at the same time, it seems to work without it, so I just changed it to
if ($.browser.webkit) {
s.modify("move", "backward", "documentboundary");
}
edit
as Tim Pointed out, modify is only available from FF4 onwards, so I took a different approach to getting the selection, which doesn't need the modify method, in hopes in making it a bit more browser compatible (IE still needs its own solution).
The code:
var r = document.createRange();
var s = window.getSelection()
var pos = 0;
function dig(el){
$(el).contents().each(function(i,e){
if (e.nodeType==1){
// not a textnode
dig(e);
}else{
if (pos<start){
if (pos+e.length>=start){
range.setStart(e, start-pos);
}
}
if (pos<end){
if (pos+e.length>=end){
range.setEnd(e, end-pos);
}
}
pos = pos+e.length;
}
});
}
var start,end, range;
function highlight(element,st,en){
range = document.createRange();
start = st;
end = en;
dig(element);
s.addRange(range);
}
highlight($('div'),3,6);
var ra = s.getRangeAt(0);
var newNode = document.createElement("em");
newNode.appendChild(ra.extractContents());
ra.insertNode(newNode);
example: http://jsfiddle.net/niklasvh/4NDb9/
Based on the ideas of the jQuery.highlight plugin.
private highlightRange(selector: JQuery, start: number, end: number): void {
let cur = 0;
let replacements: { node: Text; pos: number; len: number }[] = [];
let dig = function (node: Node): void {
if (node.nodeType === 3) {
let nodeLen = (node as Text).data.length;
let next = cur + nodeLen;
if (next > start && cur < end) {
let pos = cur >= start ? cur : start;
let len = (next < end ? next : end) - pos;
if (len > 0) {
if (!(pos === cur && len === nodeLen && node.parentNode &&
node.parentNode.childNodes && node.parentNode.childNodes.length === 1 &&
(node.parentNode as Element).tagName === 'SPAN' && (node.parentNode as Element).className === 'highlight1')) {
replacements.push({
node: node as Text,
pos: pos - cur,
len: len,
});
}
}
}
cur = next;
}
else if (node.nodeType === 1) {
let childNodes = node.childNodes;
if (childNodes && childNodes.length) {
for (let i = 0; i < childNodes.length; i++) {
dig(childNodes[i]);
if (cur >= end) {
break;
}
}
}
}
};
selector.each(function (index, element): void {
dig(element);
});
for (let i = 0; i < replacements.length; i++) {
let replacement = replacements[i];
let highlight = document.createElement('span');
highlight.className = 'highlight1';
let wordNode = replacement.node.splitText(replacement.pos);
wordNode.splitText(replacement.len);
let wordClone = wordNode.cloneNode(true);
highlight.appendChild(wordClone);
wordNode.parentNode.replaceChild(highlight, wordNode);
}
}
I know that the question is not about this relevant but this is what I was actually searching for.
If you need to Highlight SELECTED TEXT
Use the following principe: operate with Selection Range methods, like this
document.getSelection().getRangeAt(0).surroundContents(YOUR_WRAPPER_NODE) // Adds wrapper
document.getSelection().getRangeAt(0).insertNode(NEW_NODE) // Inserts a new node
That's it, I recomend you to study more about Range methods.
I was strugling with this and my searching requests were incorrect, so I decided to post it here for the case there will be people like me.
Sorry again for irelevant answer.
I'm trying to extract the exact selection and cursor location from a textarea. As usual, what's easy in most browsers is not in IE.
I'm using this:
var sel=document.selection.createRange();
var temp=sel.duplicate();
temp.moveToElementText(textarea);
temp.setEndPoint("EndToEnd", sel);
selectionEnd = temp.text.length;
selectionStart = selectionEnd - sel.text.length;
Which works 99% of the time. The problem is that TextRange.text doesn't return leading or trailing new line characters. So when the cursor is a couple of blank lines after a paragraph it yields a position at the end of the preceeding paragraph - rather than the actual cursor position.
eg:
the quick brown fox| <- above code thinks the cursor is here
| <- when really it's here
The only fix I can think of is to temporarily insert a character before and after the selection, grab the actual selection and then remove those temp characters again. It's a hack but in a quick experiment looks like it will work.
But first I'd like to be sure there's not an easier way.
I'm adding another answer since my previous one is already getting somewhat epic.
This is what I consider the best version yet: it takes bobince's approach (mentioned in the comments to my first answer) and fixes the two things I didn't like about it, which were first that it relies on TextRanges that stray outside the textarea (thus harming performance), and second the dirtiness of having to pick a giant number for the number of characters to move the range boundary.
function getSelection(el) {
var start = 0, end = 0, normalizedValue, range,
textInputRange, len, endRange;
if (typeof el.selectionStart == "number" && typeof el.selectionEnd == "number") {
start = el.selectionStart;
end = el.selectionEnd;
} else {
range = document.selection.createRange();
if (range && range.parentElement() == el) {
len = el.value.length;
normalizedValue = el.value.replace(/\r\n/g, "\n");
// Create a working TextRange that lives only in the input
textInputRange = el.createTextRange();
textInputRange.moveToBookmark(range.getBookmark());
// Check if the start and end of the selection are at the very end
// of the input, since moveStart/moveEnd doesn't return what we want
// in those cases
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 {
start: start,
end: end
};
}
var el = document.getElementById("your_textarea");
var sel = getSelection(el);
alert(sel.start + ", " + sel.end);
The move by negative bazillion seems to work perfectly.
Here's what I ended up with:
var sel=document.selection.createRange();
var temp=sel.duplicate();
temp.moveToElementText(textarea);
var basepos=-temp.moveStart('character', -10000000);
this.m_selectionStart = -sel.moveStart('character', -10000000)-basepos;
this.m_selectionEnd = -sel.moveEnd('character', -10000000)-basepos;
this.m_text=textarea.value.replace(/\r\n/gm,"\n");
Thanks bobince - how can I vote up your answer when it's just a comment :(
A jquery plugin to get selection index start and end in text area. The above javascript codes didnt work for IE7 and IE8 and gave very inconsistent results, so I have written this small jquery plugin. Allows to temporarily save start and end index of the selection and hightlight the selection at a later time.
A working example and brief version is here: http://jsfiddle.net/hYuzk/3/
A more details version with comments etc. is here: http://jsfiddle.net/hYuzk/4/
// Cross browser plugins to set or get selection/caret position in textarea, input fields etc for IE7,IE8,IE9, FF, Chrome, Safari etc
$.fn.extend({
// Gets or sets a selection or caret position in textarea, input field etc.
// Usage Example: select text from index 2 to 5 --> $('#myTextArea').caretSelection({start: 2, end: 5});
// get selected text or caret position --> $('#myTextArea').caretSelection();
// if start and end positions are the same, caret position will be set instead o fmaking a selection
caretSelection : function(options)
{
if(options && !isNaN(options.start) && !isNaN(options.end))
{
this.setCaretSelection(options);
}
else
{
return this.getCaretSelection();
}
},
setCaretSelection : function(options)
{
var inp = this[0];
if(inp.createTextRange)
{
var selRange = inp.createTextRange();
selRange.collapse(true);
selRange.moveStart('character', options.start);
selRange.moveEnd('character',options.end - options.start);
selRange.select();
}
else if(inp.setSelectionRange)
{
inp.focus();
inp.setSelectionRange(options.start, options.end);
}
},
getCaretSelection: function()
{
var inp = this[0], start = 0, end = 0;
if(!isNaN(inp.selectionStart))
{
start = inp.selectionStart;
end = inp.selectionEnd;
}
else if( inp.createTextRange )
{
var inpTxtLen = inp.value.length, jqueryTxtLen = this.val().length;
var inpRange = inp.createTextRange(), collapsedRange = inp.createTextRange();
inpRange.moveToBookmark(document.selection.createRange().getBookmark());
collapsedRange.collapse(false);
start = inpRange.compareEndPoints('StartToEnd', collapsedRange) > -1 ? jqueryTxtLen : inpRange.moveStart('character', -inpTxtLen);
end = inpRange.compareEndPoints('EndToEnd', collapsedRange) > -1 ? jqueryTxtLen : inpRange.moveEnd('character', -inpTxtLen);
}
return {start: Math.abs(start), end: Math.abs(end)};
},
// Usage: $('#txtArea').replaceCaretSelection({start: startIndex, end: endIndex, text: 'text to replace with', insPos: 'before|after|select'})
// Options start: start index of the text to be replaced
// end: end index of the text to be replaced
// text: text to replace the selection with
// insPos: indicates whether to place the caret 'before' or 'after' the replacement text, 'select' will select the replacement text
replaceCaretSelection: function(options)
{
var pos = this.caretSelection();
this.val( this.val().substring(0,pos.start) + options.text + this.val().substring(pos.end) );
if(options.insPos == 'before')
{
this.caretSelection({start: pos.start, end: pos.start});
}
else if( options.insPos == 'after' )
{
this.caretSelection({start: pos.start + options.text.length, end: pos.start + options.text.length});
}
else if( options.insPos == 'select' )
{
this.caretSelection({start: pos.start, end: pos.start + options.text.length});
}
}
});
N.B. Please refer to my other answer for the best solution I can offer. I'm leaving this here for background.
I've come across this problem and written the following that works in all cases. In IE it does use the method you suggested of temporarily inserting a character at the selection boundary, and then uses document.execCommand("undo") to remove the inserted character and prevent the insertion from remaining on the undo stack. I'm pretty sure there's no easier way. Happily, IE 9 will support the selectionStart and selectionEnd properties.
function getSelectionBoundary(el, isStart) {
var property = isStart ? "selectionStart" : "selectionEnd";
var originalValue, textInputRange, precedingRange, pos, bookmark;
if (typeof el[property] == "number") {
return el[property];
} else if (document.selection && document.selection.createRange) {
el.focus();
var range = document.selection.createRange();
if (range) {
range.collapse(!!isStart);
originalValue = el.value;
textInputRange = el.createTextRange();
precedingRange = textInputRange.duplicate();
pos = 0;
if (originalValue.indexOf("\r\n") > -1) {
// Trickier case where input value contains line breaks
// Insert a character in the text input range and use that as
// a marker
range.text = " ";
bookmark = range.getBookmark();
textInputRange.moveToBookmark(bookmark);
precedingRange.setEndPoint("EndToStart", textInputRange);
pos = precedingRange.text.length - 1;
// Executing an undo command to delete the character inserted
// prevents this method adding to the undo stack. This trick
// came from a user called Trenda on MSDN:
// http://msdn.microsoft.com/en-us/library/ms534676%28VS.85%29.aspx
document.execCommand("undo");
} else {
// Easier case where input value contains no line breaks
bookmark = range.getBookmark();
textInputRange.moveToBookmark(bookmark);
precedingRange.setEndPoint("EndToStart", textInputRange);
pos = precedingRange.text.length;
}
return pos;
}
}
return 0;
}
var el = document.getElementById("your_textarea");
var startPos = getSelectionBoundary(el, true);
var endPos = getSelectionBoundary(el, false);
alert(startPos + ", " + endPos);
UPDATE
Based on bobince's suggested approach in the comments, I've created the following, which seems to work well. Some notes:
bobince's approach is simpler and shorter.
My approach is intrusive: it makes changes to the input's value before reverting those changes, although there is no visible effect of this.
My approach has the advantage of keeping all operations within the input. bobince's approach relies on creating ranges that span from the start of the body to the current selection.
A consequence of 3. is that the performance of bobince's varies with the position of the input within the document whereas mine does not. My simple tests suggest that when the input is close to the start of the document, bobince's approach is significantly faster. When the input is after a significant chunk of HTML, my approach is faster.
function getSelection(el) {
var start = 0, end = 0, normalizedValue, textInputRange, elStart;
var range = document.selection.createRange();
var bigNum = -1e8;
if (range && range.parentElement() == el) {
normalizedValue = el.value.replace(/\r\n/g, "\n");
start = -range.moveStart("character", bigNum);
end = -range.moveEnd("character", bigNum);
textInputRange = el.createTextRange();
range.moveToBookmark(textInputRange.getBookmark());
elStart = range.moveStart("character", bigNum);
// Adjust the position to be relative to the start of the input
start += elStart;
end += elStart;
// Correct for line breaks so that offsets are relative to the
// actual value of the input
start += normalizedValue.slice(0, start).split("\n").length - 1;
end += normalizedValue.slice(0, end).split("\n").length - 1;
}
return {
start: start,
end: end
};
}
var el = document.getElementById("your_textarea");
var sel = getSelection(el);
alert(sel.start + ", " + sel.end);