Single-click only works once - javascript

I want to allow the user to remove words in a div by a single mouse click. It work fine, see jsFiddle.
The only problem is that the single-click feature only works on the first click. After that, you need to double click.
I can't get my head around why it is behaving like this. Maybe you can? Could be problem with jQuery(document).ready()...
jQuery:
// highlight a word/term quicker and smarter: so.com/a/35103840/1185126
jQuery(document).ready(function(e){
(function(els){
// variable declaration for previous range info
// and function for finding the sibling
var prevRangeInfo = {},
findSibling = function(thisNode, direction){
// get the child node list of the parent node
var childNodeList = thisNode.parentNode.childNodes,
children = [];
// convert the child node list to an array
for(var i=0, l=childNodeList.length; i<l; i++) children.push(childNodeList[i]);
return children[children.indexOf(thisNode) + direction];
};
for(var i=0;i<els.length;i++){
var el = els[i];
el.addEventListener('mouseup',function(evt){
if (document.createRange) { // Works on all browsers, including IE 9+
var selected = window.getSelection();
// Removing the following line from comments will make the function drag-only
/* if(selected.toString().length){ */
var d = document,
nA = selected.anchorNode,
oA = selected.anchorOffset,
nF = selected.focusNode,
oF = selected.focusOffset,
range = d.createRange(),
rangeLength = 0;
range.setStart(nA,oA);
range.setEnd(nF,oF);
// Check if direction of selection is right to left
if(range.startContainer !== nA || (nA === nF && oF < oA)){
range.setStart(nF,oF);
range.setEnd(nA,oA);
}
// Extend range to the next space or end of node
while(range.endOffset < range.endContainer.textContent.length && !/\s$/.test(range.toString())){
range.setEnd(range.endContainer, range.endOffset + 1);
}
// Extend range to the previous space or start of node
while(range.startOffset > 0 && !/^\s/.test(range.toString())){
range.setStart(range.startContainer, range.startOffset - 1);
}
// Remove spaces
if(/\s$/.test(range.toString()) && range.endOffset > 0)
range.setEnd(range.endContainer, range.endOffset - 1);
if(/^\s/.test(range.toString()))
range.setStart(range.startContainer, range.startOffset + 1);
// Store the length of the range
rangeLength = range.toString().length;
// Check if another range was previously selected
if(prevRangeInfo.startContainer && nA === nF && oA === oF){
var rangeTryContain = d.createRange(),
rangeTryLeft = d.createRange(),
rangeTryRight = d.createRange(),
nAp = prevRangeInfo.startContainer;
oAp = prevRangeInfo.startOffset;
nFp = prevRangeInfo.endContainer;
oFp = prevRangeInfo.endOffset;
rangeTryContain.setStart(nAp, oAp);
rangeTryContain.setEnd(nFp, oFp);
rangeTryLeft.setStart(nFp, oFp-1);
rangeTryLeft.setEnd(range.endContainer, range.endOffset);
rangeTryRight.setStart(range.startContainer, range.startOffset);
rangeTryRight.setEnd(nAp, oAp+1);
// Store range boundary comparisons
// & inner nodes close to the range boundary --> stores null if none
var compareStartPoints = range.compareBoundaryPoints(0, rangeTryContain) === 0,
compareEndPoints = range.compareBoundaryPoints(2, rangeTryContain) === 0,
leftInnerNode = range.endContainer.previousSibling,
rightInnerNode = range.startContainer.nextSibling;
// Do nothing if clicked on the right end of a word
if(range.toString().length < 1){
range.setStart(nAp,oAp);
range.setEnd(nFp,oFp);
}
// Collapse the range if clicked on last highlighted word
else if(compareStartPoints && compareEndPoints)
range.collapse();
// Remove a highlighted word from left side if clicked on
// This part is quite tricky!
else if(compareStartPoints){
range.setEnd(nFp,oFp);
if(range.startOffset + rangeLength + 1 >= range.startContainer.length){
if(rightInnerNode)
// there is a right inner node, set its start point as range start
range.setStart(rightInnerNode.firstChild, 0);
else {
// there is no right inner node
// there must be a text node on the right side of the clicked word
// set start of the next text node as start point of the range
var rightTextNode = findSibling(range.startContainer.parentNode, 1),
rightTextContent = rightTextNode.textContent,
level=1;
// if beginning of paragraph, find the first child of the paragraph
if(/^(?:\r\n|[\r\n])|\s{2,}$/.test(rightTextContent)){
rightTextNode = findSibling(rightTextNode, 1).firstChild;
level--;
}
range.setStart(rightTextNode, level);
}
}
else
range.setStart(range.startContainer, range.startOffset + rangeLength + 1);
}
// Remove a hightlighted word from right side if clicked on
// This part is also tricky!
else if (compareEndPoints){
range.setStart(nAp,oAp);
if(range.endOffset - rangeLength - 1 <= 0){
if(leftInnerNode)
// there is a right inner node, set its start point as range start
range.setEnd(leftInnerNode.lastChild, leftInnerNode.lastChild.textContent.length);
else {
// there is no left inner node
// there must be a text node on the left side of the clicked word
// set start of the previous text node as start point of the range
var leftTextNode = findSibling(range.endContainer.parentNode, -1),
leftTextContent = leftTextNode.textContent,
level = 1;
// if end of paragraph, find the last child of the paragraph
if(/^(?:\r\n|[\r\n])|\s{2,}$/.test(leftTextContent)){
leftTextNode = findSibling(leftTextNode, -1).lastChild;
level--;
}
range.setEnd(leftTextNode, leftTextNode.length - level);
}
}
else
range.setEnd(range.endContainer, range.endOffset - rangeLength - 1);
}
// Add previously selected range if adjacent
// Upgraded to include previous/next word even in a different paragraph
else if(/^[^\s]*((?:\r\n|[\r\n])|\s{1,})[^\s]*$/.test(rangeTryLeft.toString()))
range.setStart(nAp,oAp);
else if(/^[^\s]*((?:\r\n|[\r\n])|\s{1,})[^\s]*$/.test(rangeTryRight.toString()))
range.setEnd(nFp,oFp);
// Detach the range objects we are done with, clear memory
rangeTryContain.detach();
rangeTryRight.detach();
rangeTryLeft.detach();
}
// Save the current range --> not the whole Range object but what is neccessary
prevRangeInfo = {
startContainer: range.startContainer,
startOffset: range.startOffset,
endContainer: range.endContainer,
endOffset: range.endOffset
};
// Clear the saved range info if clicked on last highlighted word
if(compareStartPoints && compareEndPoints)
prevRangeInfo = {};
// Remove all ranges from selection --> necessary due to potential removals
selected.removeAllRanges();
// Assign the current range as selection
selected.addRange(range);
// Detach the range object we are done with, clear memory
range.detach();
el.style.MozUserSelect = '-moz-none';
// Removing the following line from comments will make the function drag-only
/* } */
} else {
// Fallback for Internet Explorer 8 and earlier
// (if you think it still is worth the effort of course)
}
});
/* This part is necessary to eliminate a FF specific dragging behavior */
el.addEventListener('mousedown',function(e){
if (window.getSelection) { // Works on all browsers, including IE 9+
var selection = window.getSelection ();
selection.collapse (selection.anchorNode, selection.anchorOffset);
} else {
// Fallback for Internet Explorer 8 and earlier
// (if you think it still is worth the effort of course)
}
el.style.MozUserSelect = 'text';
});
}
})(document.getElementsByClassName('taggable'));
});
// remove selected text
jQuery(document).ready(function() {
jQuery('.taggable').bind("mouseup", function() {
var text1;
if (window.getSelection().toString() != "") {
selectedText = window.getSelection().toString()
text1 = jQuery(".taggable").text().split("")
pointStart = window.getSelection().anchorOffset
pointEnd = window.getSelection().focusOffset
if (pointEnd < pointStart) {
pointStart = pointEnd
}
text1.splice(pointStart, selectedText.length);
text1 = text1.join("")
} else {
selectedText = jQuery(".taggable").text()
text1 = selectedText;
}
jQuery(".taggable").text(text1);
});
});

This might not solve your issue, but it's another approach.
This code wrap every word inside a span and create an event listener for each.
HTML
<p>This is an example text</p>
Javascript
jQuery(document).ready(function($) {
var text = $('p').text();
var arr = text.split(' ');
$('p').html('');
for (var i = 0; i < arr.length; i++) {
$('<span />').html(arr[i] + ' ').appendTo('p');
$('p').on('click', 'span:nth-of-type(' + (i + 1) + ')', function() {
$(this).remove();
});
}
});

I've figured out that the main problem is because if the anchorOffset is equal to focusOffset, it doesn't work, so, the possible solution is to add +1 when it's equal and then the code will work as desired since it finds at least a letter when substring. Change the code:
from
var selected = window.getSelection();
// Removing the following line from comments will make the function drag-only
/* if(selected.toString().length){ */
var d = document,
nA = selected.anchorNode,
oA = selected.anchorOffset,
nF = selected.focusNode,
oF = selected.focusOffset,
range = d.createRange(),
rangeLength = 0;
to
var selected = window.getSelection();
var offset = selected.focusOffset;
if(selected.anchorOffset == selected.focusOffset)
offset++;
// Removing the following line from comments will make the function drag-only
/* if(selected.toString().length){ */
var d = document,
nA = selected.anchorNode,
oA = selected.anchorOffset,
nF = selected.focusNode,
oF = offset,
range = d.createRange(),
rangeLength = 0;
I've tested many times here on JFiddle and it worked OK, but
I'm still afraid that this can result in some other problems.
If you get some problem, please notify me and i'll help.
EDIT:
jsFiddle

Related

character position of a multi-line selection within a content editable div using javascript

Please take a look at this fiddle:
https://jsfiddle.net/darrengates/jwx41Lkz/
In this example, I have an editable div with 3 lines. I'm attempting to get the start character position, and end character position, of a selection.
This appears to work for the first line only. If I highlight more than 1 line, the values are incorrect (see the comments for an explanation).
How do I get the start and end character position of a selection when it spans multiple lines? (i.e., the character position starting at the first letter of the entire div, so for example the "s" in "second line" would be approximately character position 11).
Here's what I have so far (runnable in the fiddle):
<div id="test" contenteditable="true" class="selecttest">
first line<br />
second line<br />
third line
</div>
.selecttest {
width: 400px;
height: 200px;
border: 1px solid black;
padding: 5px;
font-size: 20px;
}
function getSelection() {
const sel = document.getSelection() || window.getSelection();
// note: if I highlight the 2nd and 3rd lines
// start and end values are 1, 11
// they should be about 11 and 22 (or so)
// on account of the existence of the first line
let start = sel.baseOffset
let end = sel.extentOffset
// if selected from right to left, reverse...
if ( start > end ) {
const temp = start;
start = end;
end = temp;
}
console.log('start, end', start, end)
}
$("#test").on('mouseup', function() {
getSelection();
})
It seems that there are many S.O. posts relating to getting selected text, but answers relating to getting the selected character positions all fail when selecting multiple lines.
Since you want to measure position in a range, you should be using a range object rather than the selection object.
A proposal on this code pen > https://codepen.io/aSH-uncover/pen/ExEdXwy
The main trick is to address the two scenarios where the range is inside the same container or not :)
function getSelection() {
const sel = document.getSelection() || window.getSelection();
const range = sel.getRangeAt(0)
let rangeContainer = range.commonAncestorContainer
let same = false
if (rangeContainer === range.startContainer) {
same = true
rangeContainer = range.startContainer.parentElement
}
let start = -1
let end = -1
const list = rangeContainer.childNodes
let offset = 0
for (let i = 0 ; i < list.length ; i++) {
const current = list[i]
if (current === range.startContainer) {
start = offset + range.startOffset
if (same) {
end = offset + range.endOffset
break
}
} else if (current === range.endContainer) {
end = offset + range.endOffset
break
}
if (current.nodeName === '#text') {
offset += current.length
}
}
console.log('start, end', start, end)
}
Edit : if you want to support more complex text (like a div with spans) you will have to improve this function and include some recursive stuff :)

How (break; while) and (continue; loop) [closed]

Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 5 years ago.
Improve this question
HHi, I have a small problem, difficult to explain, but I have a while inside a loop.
the context of my plugin calls for a reset of the loop. (continue)
however, I will also need to do (break) in the while.
How i can Break the while and call Continue; for the loop.
here the full context, I know it's hard to read code from a custom context
but here my function scope and what am try target.
look at this line console.log('=> how break and continue')
thank for help
// STEP3: BUILD TEXT CHILD
var re = /\w+.|\W/g; // \w+.\s*|\W+ // if wordWrap only
var l = -1; // START AT LINE -1 (line++ at start loop)
var lineX = 0;
var lineY = 0;
var lineHeight = 0; // at end maxheight auto increase
var newLine=true; // jmp line and creat
var dataLine = [];
var maxLineX = this._style.global.wordWrap && this._style.global.wordWrapWidth || false;
var pixiBox = false;;
for(var d=0, newLine=true; d<dataTxt.length; d++){ // loop all dataTxt
var data = dataTxt[d];
var dataL = dataLine[l];
if(newLine){ // INITIALISE NEW LINE AND RESET DATA
lineY+=lineHeight;
lineX = 0;
lineHeight = 0;
newLine = false;
l++;
dataLine[l] = {width:0, height:0, txtID:[], line:l }; // initialise a new this._dataLines [line]
var dataL = dataLine[l];
};
// create type of elements
if(!pixiBox){ // if empty pixi box, create new data pixi
if(data.type==='txt'){
pixiBox = new PIXI.Text(data.value, data.style);
}else if(data.type==='icon'){ // if icons was not registered, registe to pixiBox;
pixiBox = new PIXI.Sprite(pixiMS._iconsID[data.value]);
}else if(data.type==='jmp'){
pixiBox = new PIXI.Graphics();
pixiBox.drawRect(0, 0, 0, dataL.height+data.value);
newLine = true;
};
};
// check if its ou limit ?
if(maxLineX && (lineX + pixiBox.width) > maxLineX){
if(d>5000){confirm('ERROR EXCEED LIMIT WORDWRAP < WORD LENGTH, OR USE BREAK WORDS'); break; }; // protection freeze engine (win) if wrong wordWrap size
if(data.type==='icon'){newLine = true; d--; continue; }; // reset to a newLine, but keep icons registered
// its a text need Split to a new line;
var letterWidth = pixiBox.width / data.value.length; // calculate width of all letter
var tmpW = 0; // Temps Width
while ( (match = re.exec(data.value) ) !== null) {
var mL = match[0].length; // match txt length
if(lineX+tmpW+(mL*letterWidth) > maxLineX){ // if this match exeed , txt befor index become this valur and add extra data after
if(match.index===0){ // if match index 0 , (this data continue) reset new line
console.log('=> how break and continue');newLine = true; d--; break; continue;
}else{ // current txt become txt befor , and push new data after
data.value = match.input.slice(0, match.index); // text befor the match (if 0 its ok, just empty txt)
var after1 = this._newData('jmp', 0, "wordWrapBreak", false);
var after2 = this._newData('txt', match.input.slice(match.index), data.tag, data.style);
this._dataTxt.splice(d+1, 0, after1,after2); // reconfigu the loop length
pixiBox.text = data.value; // redefine pixi
break;
}
};
tmpW+=lineX;
};
};
data.xPos = lineX;
data.yPos = lineY;
data.line = l;
// INCREASE TXT POSITION
lineX += pixiBox.width; // x pos for next pixi element
dataL.width = lineX; // line width ++ valueOf()
lineHeight = pixiBox.height>lineHeight&&pixiBox.height||lineHeight;
dataL.height = lineHeight;
dataL.txtID.push(d);
this._childTexts.push(pixiBox); // store child array
this.addChild(pixiBox);
pixiBox = false;
};
return this;
edit Solved:
if(maxLineX && (lineX + pixiBox.width) > maxLineX){
if(d>5000){confirm('ERROR EXCEED LIMIT WORDWRAP < WORD LENGTH, OR USE BREAK WORDS'); break; }; // protection freeze engine (win) if wrong wordWrap size
if(data.type==='icon'){newLine = true; d--; continue; }; // reset to a newLine, but keep icons registered
// its a text need Split to a new line;
var letterWidth = pixiBox.width / data.value.length; // calculate width of all letter
var tmpW = 0; // Temps Width
var skip = false;
while ( (match = re.exec(data.value) ) !== null) {
var mL = match[0].length; // match txt length
if(lineX+tmpW+(mL*letterWidth) > maxLineX){ // if this match exeed , txt befor index become this valur and add extra data after
if(match.index===0){ // if match index 0 , (this data continue) reset new line
console.log('=> how break and continue');newLine = true; d--; skip=true; break;
}else{ // current txt become txt befor , and push new data after
data.value = match.input.slice(0, match.index); // text befor the match (if 0 its ok, just empty txt)
var after1 = this._newData('jmp', 0, "wordWrapBreak", false);
var after2 = this._newData('txt', match.input.slice(match.index), data.tag, data.style);
this._dataTxt.splice(d+1, 0, after1,after2); // reconfigu the loop length
pixiBox.text = data.value; // redefine pixi
break;
}
};
if(skip){continue;}
tmpW+=lineX;
};
};
What you need are labels.
forloop:
for(){
whileloop:
while(){
break whileloop;
}
}
This will exit the while loop but remain in the for loop. Read more here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/label
This is ugly IMO, but you can do:
outer: for(var d=0, newLine=true; d<dataTxt.length; d++){ // loop all dataTxt
//...
while ( (match = re.exec(data.value) ) !== null) {
//...
if(match.index===0){
continue outer;
}
}
}
What I would probably do is:
for(var d=0, newLine=true; d<dataTxt.length; d++){ // loop all dataTxt
//...
var skip = false;
while ( (match = re.exec(data.value) ) !== null) {
if(match.index===0){
skip = true;
break;
}
}
if (skip) {
continue;
}
//...
}

Remove whitespace from window selection in js

I'm trying to remove leading or trailing (sometimes both) whitespace from the user selected text. Implemented according to this answer. This works for simple cases, however, when the selected text contains tags or , it fails.
Example: in the fiddle try to highlight -- hi this is bob. from right to left, including the space at the end, then press Trim.
This results in:
Uncaught IndexSizeError: Failed to execute 'setEnd' on 'Range': The offset 24 is larger than or equal to the node's length (5).
I guess this can be caught with
if (method == range.setEnd && range.startOffset + ind >= range.endContainer.length)
but I'm not sure how to handle it.
I've also tried to replace the hard spaces using
e2 = document.getElementById('e2');
e2.innerHTML = e2.innerHTML.replace(/ /gi, ' ');
However, this makes the selection empty.
Code:
function removeWsFromSelection(fromStart) {
selection = window.getSelection();
range = selection.getRangeAt(0);
if (fromStart) {
regex = /[^\s]/;
container = range.startContainer;
method = range.setStart;
} else {
regex = /\s+$/;
container = range.endContainer;
method = range.setEnd;
}
match = regex.exec(selection.toString());
if (match) {
ind = match.index;
if (ind > 0) {
// ind is the first non-ws char from the start or first ws char from the end,
// hence (startOffset + ind)
method.call(range, container, range.startOffset + ind);
rng = range.cloneRange();
selection.removeAllRanges();
selection.addRange(rng);
}
}
}
BTW, unfortunately, Selection.modify didn't work for me, and besides it's considered non-standard.
If you are fine with range modifying, you're able to check the length of trimmed symbols from the start and the end, and then modify startOffset and endOffset of the Range. But sure this is not a silver bullet for a cases where startContainer and endContainer are not the same node. At least it works for a certain of cases.
const sel = window.getSelection();
const text = sel.toString();
const range = sel.getRangeAt(0);
const startOffset = text.length - text.trimStart().length;
const endOffset = text.length - text.trimEnd().length;
if (startOffset) {
range.setStart(range.startContainer, range.startOffset + startOffset);
}
if (endOffset) {
range.setEnd(range.endContainer, range.endOffset - endOffset);
}
This is very ugly and doesn't handle the general case, but seems to work:
function removeWsFromSelection(fromStart) {
selection = window.getSelection();
range = selection.getRangeAt(0);
if (fromStart) {
regex = /[^\s]/;
container = range.startContainer;
method = range.setStart;
}
else {
regex = /\s+$/;
container = range.endContainer;
method = range.setEnd;
}
match = regex.exec(selection.toString());
if (match) {
ind = match.index;
if (ind > 0) {
// ind is the first non-ws char from the start or first ws char from the end,
// hence (startOffset + ind)
if (method == range.setEnd && range.startOffset + ind >= range.endContainer.length) {
match = regex.exec(range.endContainer.textContent);
if (match) {
range.setEnd(range.endContainer, match.index);
}
}
else {
method.call(range, container, range.startOffset + ind);
}
rng = range.cloneRange();
selection.removeAllRanges();
selection.addRange(rng);
}
}
}
I've modified Micheal's answer to work with multiple ranges and ranges that cross node boundaries. This seems to trim selections consistently in all my tests, but there's always room for edge cases I'm sure.
TypeScript, but easy enough to adapt to vanilla JS.
export function trimRanges(selection: Selection) {
for (let i = 0, range = selection.getRangeAt(0); i < selection.rangeCount; range = selection.getRangeAt(i++)) {
const text = selection.toString();
const startOffset = text.length - text.trimStart().length;
const endOffset = text.length - text.trimEnd().length;
if (startOffset) {
const offset = range.startOffset + startOffset;
if (offset < 0) {
// If the range will underflow the current element, then it belongs in the previous element
const start = range.startContainer.parentElement.previousSibling;
range.setStart(start, start.textContent.length + offset);
} else if (offset > range.startContainer.textContent.length) {
// If the range will overflow the current element, then it belongs in the next element
const start = range.startContainer.parentElement.nextSibling;
range.setStart(start, offset - range.startContainer.textContent.length);
} else {
range.setStart(range.startContainer, offset);
}
}
if (endOffset) {
const offset = range.endOffset - endOffset;
if (offset < 0) {
// If the range will underflow the current element, then it belongs in the previous element
const end = range.endContainer.parentElement.previousSibling;
range.setEnd(end, end.textContent.length + offset);
} else if (offset > range.endContainer.textContent.length) {
// If the range will overflow the current element, then it belongs in the next element
const end = range.endContainer.parentElement.nextSibling;
range.setEnd(end, offset - range.endContainer.textContent.length);
} else {
range.setEnd(range.endContainer, offset);
}
}
}
}
Note that this does modify the ranges attached to the selection. If you need atomicity, you could hypothetically clone each range before editing it, remove all ranges, then add the cloned ranges back to the selection, but I haven't found any issues with the approach as it stands.

Select spans accross multiple divs issue

I'm having the following issue - I'm trying to select the text inside spans located across multiple divs. To give an example
<div>asd<span>fgh</span></div>
<div><span>qwerty</span></div>
<div><span>uio</span>asd</div>
Now in this scenario, if the user clicks somewhere inside the word qwerty I'd like to select the text 'fghqwertuio' --> all the adjacent spans. I'm using the following code to do this:
var range = document.caretRangeFromPoint(lastTappedX, lastTappedY);
range.selectNodeContents(range.startContainer);
window.getSelection().addRange(range);
var containerNodes = document.body.children[0].children;
var whichChild = -1;
for ( var i = 0; i < containerNodes.length; ++i) {
if (containerNodes[i] === range.startContainer.parentNode.parentNode) {
whichChild = i;
break;
}
}
if (whichChild === -1) {
console.log("couldn't find the highlighted div");
}
// go right the dom tree
for ( var i = whichChild + 1; i < containerNodes.length; ++i) {
var containerChildren = containerNodes[i].children;
if (containerChildren[0]
&& containerChildren[0].style['background-color']) {
var newRange = document.createRange();
newRange.selectNodeContents(containerChildren[0]);
window.getSelection().addRange(newRange);
}
if (containerChildren.length > 1) {
break;
}
}
// go left the down tree
for ( var i = whichChild - 1; i >= 0; --i) {
var containerChildren = containerNodes[i].children;
if (containerChildren[containerChildren.length - 1].style['background-color']) {
var newRange = document.createRange();
newRange
.selectNodeContents(containerChildren[containerChildren.length - 1]);
window.getSelection().addRange(newRange);
}
if (containerChildren.length > 1) {
break;
}
}
When I log what happens - I'm correctly creating ranges containing the text I'd like to select but adding them to the selection object doesn't seem to work. The current selection is only the first added range. Any help on how to solve this will be greatly appreciated.
Of the major browsers, only Firefox allows multiple ranges per selection. In all other browsers you're limited to one range.
You need to tweak your code to create one range and use the range's setStart() and setEnd() methods. Also, properties of the style property of elements use camel case rather than hyphens (i.e. .backgroundColor rather than ['background-color']).

How to get the start and end points of selection in text area?

I want to get the cursor start and end position of a selected range in a text-field or text-area.
I tried lot of functions in various forums, but when the last character of the selection is a new line character JavaScript ignore it in IE6.
How do I get the start and end points of the selection?
Revised answer, 5 September 2010
To account for trailing line breaks is tricky in IE, and I haven't seen any solution that does this. It is possible, however. The following is a new version of what I previously posted here.
Note that the textarea must have focus for this function to work properly in IE. If in doubt, call the textarea's focus() method first.
function getInputSelection(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
};
}
Use the Rangy api and all of your problems are gone gone gone gone...
Using it
Read the documentation, or just use the below.
Very simple,
var selection = rangy.getSelection(), // Whole lot of information, supports
// multi-selections too.
start = selection.anchorOffset, // Start position
end = selection.focusOffset; // End position
Hope this api helps you out because it is really helpful in handling cross-browser ranges.
I needed to do something very similar and came up with this:
function getSelection(target) {
var s = {start: 0, end:0};
if (typeof target.selectionStart == "number"
&& typeof target.selectionEnd == "number") {
// Firefox (and others)
s.start = target.selectionStart;
s.end = target.selectionEnd;
} else if (document.selection) {
// IE
var bookmark = document.selection.createRange().getBookmark();
var sel = target.createTextRange();
var bfr = sel.duplicate();
sel.moveToBookmark(bookmark);
bfr.setEndPoint("EndToStart", sel);
s.start = bfr.text.length;
s.end = s.start + sel.text.length;
}
return s;
}
Notes:
The sel range must be created by
target rather than using the range
returned by document.selection,
otherwise bfr.setEndPoint will
complain about an invalid argument.
This "feature" (discovered
in IE8) does not appear to be
documented in the spec.
target must have input focus for this function to work.
Only tested with <textarea>, might work with <input> as well.

Categories

Resources