I have a contenteditable div as follow (| = cursor position):
<div id="mydiv" contenteditable="true">lorem ipsum <spanclass="highlight">indol|or sit</span> amet consectetur <span class='tag'>adipiscing</span> elit</div>
I would like to get the current cursor position including html tags. My code :
var offset = document.getSelection().focusOffset;
Offset is returning 5 (full text from the last tag) but i need it to handle html tags. The expected return value is 40. The code has to work with all recents browsers.
(i also checked this : window.getSelection() offset with HTML tags? but it doesn't answer my question).
Any ideas ?
Another way to do it is by adding a temporary marker in the DOM and calculating the offset from this marker. The algorithm looks for the HTML serialization of the marker (its outerHTML) within the inner serialization (the innerHTML) of the div of interest. Repeated text is not a problem with this solution.
For this to work, the marker's serialization must be unique within its div. You cannot control what users type into a field but you can control what you put into the DOM so this should not be difficult to achieve. In my example, the marker is made unique statically: by choosing a class name unlikely to cause a clash ahead of time. It would also be possible to do it dynamically, by checking the DOM and changing the class until it is unique.
I have a fiddle for it (derived from Alvaro Montoro's own fiddle). The main part is:
function getOffset() {
if ($("." + unique).length)
throw new Error("marker present in document; or the unique class is not unique");
// We could also use rangy.getSelection() but there's no reason here to do this.
var sel = document.getSelection();
if (!sel.rangeCount)
return; // No ranges.
if (!sel.isCollapsed)
return; // We work only with collapsed selections.
if (sel.rangeCount > 1)
throw new Error("can't handle multiple ranges");
var range = sel.getRangeAt(0);
var saved = rangy.serializeSelection();
// See comment below.
$mydiv[0].normalize();
range.insertNode($marker[0]);
var offset = $mydiv.html().indexOf($marker[0].outerHTML);
$marker.remove();
// Normalizing before and after ensures that the DOM is in the same shape before
// and after the insertion and removal of the marker.
$mydiv[0].normalize();
rangy.deserializeSelection(saved);
return offset;
}
As you can see, the code has to compensate for the addition and removal of the marker into the DOM because this causes the current selection to get lost:
Rangy is used to save the selection and restore it afterwards. Note that the save and restore could be done with something lighter than Rangy but I did not want to load the answer with minutia. If you decide to use Rangy for this task, please read the documentation because it is possible to optimize the serialization and deserialization.
For Rangy to work, the DOM must be in exactly the same state before and after the save. This is why normalize() is called before we add the marker and after we remove it. What this does is merge immediately adjacent text nodes into a single text node. The issue is that adding a marker to the DOM can cause a text node to be broken into two new text nodes. This causes the selection to be lost and, if not undone with a normalization, would cause Rangy to be unable to restore the selection. Again, something lighter than calling normalize could do the trick but I did not want to load the answer with minutia.
EDIT: This is an old answer that doesn't work for OP's requirement of having nodes with the same text. But it's cleaner and lighter if you don't have that requirement.
Here is one option that you can use and that works in all major browsers:
Get the offset of the caret within its node (document.getSelection().anchorOffset)
Get the text of the node in which the caret is located (document.getSelection().anchorNode.data)
Get the offset of that text within #mydiv by using indexOf()
Add the values obtained in 1 and 3, to get the offset of the caret within the div.
The code would look like this for your particular case:
var offset = document.getSelection().anchorOffset;
var text = document.getSelection().anchorNode.data;
var textOffset = $("#mydiv").html().indexOf( text );
offsetCaret = textOffset + offset;
You can see a working demo on this JSFiddle (view the console to see the results).
And a more generic version of the function (that allows to pass the div as a parameter, so it can be used with different contenteditable) on this other JSFiddle:
function getCaretHTMLOffset(obj) {
var offset = document.getSelection().anchorOffset;
var text = document.getSelection().anchorNode.data;
var textOffset = obj.innerHTML.indexOf( text );
return textOffset + offset;
}
About this answer
It will work in all recent browsers as requested (tested on Chrome 42, Firefox 37, and Explorer 11).
It is short and light, and doesn't require any external library (not even jQuery)
Issue: If you have different nodes with the same text, it may return the offset of the first occurrence instead of the real position of the caret.
NOTE: This solution works even in nodes with repeated text, but it detects html entities (e.g.: ) as only one character.
I came up with a completely different solution based on processing the nodes. It is not as clean as the old answer (see other answer), but it works fine even when there are nodes with the same text (OP's requirement).
This is a description of how it works:
Create a stack with all the parent elements of the node in which the caret is located.
While the stack is not empty, traverse the nodes of the containing element (initially the content editable div).
If the node is not the same one at the top of the stack, add its size to the offset.
If the node is the same as the one at the top of the stack: pop it from the stack, go to step 2.
The code is like this:
function getCaretOffset(contentEditableDiv) {
// read the node in which the caret is and store it in a stack
var aux = document.getSelection().anchorNode;
var stack = [ aux ];
// add the parents to the stack until we get to the content editable div
while ($(aux).parent()[0] != contentEditableDiv) { aux = $(aux).parent()[0]; stack.push(aux); }
// traverse the contents of the editable div until we reach the one with the caret
var offset = 0;
var currObj = contentEditableDiv;
var children = $(currObj).contents();
while (stack.length) {
// add the lengths of the previous "siblings" to the offset
for (var x = 0; x < children.length; x++) {
if (children[x] == stack[stack.length-1]) {
// if the node is not a text node, then add the size of the opening tag
if (children[x].nodeType != 3) { offset += $(children[x])[0].outerHTML.indexOf(">") + 1; }
break;
} else {
if (children[x].nodeType == 3) {
// if it's a text node, add it's size to the offset
offset += children[x].length;
} else {
// if it's a tag node, add it's size + the size of the tags
offset += $(children[x])[0].outerHTML.length;
}
}
}
// move to a more inner container
currObj = stack.pop();
children = $(currObj).contents();
}
// finally add the offset within the last node
offset += document.getSelection().anchorOffset;
return offset;
}
You can see a working demo on this JSFiddle.
About this answer:
It works in all major browsers.
It is light and doesn't require external libraries (apart from jQuery)
It has an issue: html entities like are counted as one character only.
Related
I am using a mouseup event to trigger a function which highlights text and surrounds the highlighted text with a span (function from stack overflow):
function highlightText(e) {
var t = window.getSelection().toString();
if (t) {
$("#mySpan").remove();
var range = window.getSelection().getRangeAt(0);
newNode = document.createElement("span");
newNode.id = 'mySpan';
range.surroundContents(newNode);
}
}
The main problem I am encountering is that as long as surroundContents is included, the text remains highlighted only about 20% of the highlight attempts (otherwise highlighting disappears immediately). I tried adding a setTimeout, not calling surroundContent for 1s. I also tried removing the remove() statement, but still no good.
Any ideas on why this is happening?
I was facing the same problem with Chromium on Android. In some specific cases, the call of range.surroundContents(newNode) would cause a very weird behaviour of page reload and so on. After checking the documentation of the function:
This method is nearly equivalent to
newNode.appendChild(range.extractContents());
range.insertNode(newNode). After surrounding, the boundary points of
the range include newNode.
So the obvious thing was to apply another way highlight the text. I found mark.js library which did exactly what I wanted without that annoying side effect. (Here's a JSFiddle sample that shows how it's used to highlight just selection). The difference is that library was not using range.surroundContents(newNode) nor newNode.appendChild but rather node.replaceChild.
Based on that, here's the solution to the problem I was having and I think it applies to your case as well.
function surroundRangeWithSpan(range) {
var span = document.createElement('span');
// The text is within the same node (no other html elements inside of it)
if (range.startContainer.isEqualNode(range.endContainer) && range.startContainer.childNodes.length == 0) {
// Here you customise your <span> element
customSurroundContents(range, span);
} else {
// Here you have to break the selection down
}
return span;
}
function customSurroundContents(range, span) {
var node = range.commonAncestorContainer;
var startNode = node.splitText(range.startOffset);
var ret = startNode.splitText(range.toString().length);
span.textContent = startNode.textContent;
startNode.parentNode.replaceChild(span, startNode);
}
And you pass window.getSelection().getRangeAt(0) to the function.
The likely cause of the failure is the selected text encompasses only the beginning or the ending of a non-text node, and not both of them.
So if were to run that code only selecting "This is Bo" in the following it will fail (and throw an exception) because it doesn't also capture the closing tag in the selection:
This is <em>bold</em>
So ending up with:
This is <em>bo
Reference: https://developer.mozilla.org/en-US/docs/Web/API/Range/surroundContents
So I have a codemirror instance on the left of a document, and an iframe on the right. When the code is updated in the editor, it is written to the iframe.
During this rewrite, I add an index to each and every element that is created using jQuery's $.data function so that whenever the user hovers their mouse over the element it can be highlighted in the editor.
So far I have managed to pick out the required element's position in the editor in terms of where its generated <span class="cm-tag cm-bracket"> tag is and give it a class of cm-custom-highlight.
My question is - is there any way to turn an instance of a source span tag into an actual selection within the editor?
Update: Answered my own question - see below! You can check out my resulting code here.
I answered my own question! How about that?
Turns out CodeMirror has a neat little list of nodes in its display container. All I needed to do was loop through CodeMirror.display.renderedView[lineNumber].measure.map and test each text node's parentNode property to see if it was the same as the span I had highlighted.
The map array is structured like so:
[
0: 0
1: 1
2: text
3: 1
4: 5
...
]
Every text node here refers to a piece of code in the editor and the numbers before and after refer to its character index, so it was pretty easy to find the index that I needed:
var span = $('span.cm-custom-highlight', CodeMirror.display.lineDiv),
lineNumber = span.closest('.CodeMirror-line').closest('div[style]').index(),
lineView = CodeMirror.display.renderedView[lineNumber].measure.map,
char = 0;
for(var i in lineView.measure.map)
{
if(!char &&
typeof lineView.measure.map[i] == 'object' &&
lineView.measure.map[i].parentNode && span[0] == lineView.measure.map[i].parentNode)
{
char = lineView.measure.map[i - 1];
}
}
Sure it's a little messy, but it gets the job done nicely.
You will get better results when using markText rather than directly messing with the editor's DOM. The DOM and view data structure aren't part of the interface, and will change between versions. The editor can also update its DOM at any moment, overriding the changes you made.
I have a contenteditable div as follow (| = cursor position):
<div id="mydiv" contenteditable="true">lorem ipsum <spanclass="highlight">indol|or sit</span> amet consectetur <span class='tag'>adipiscing</span> elit</div>
I would like to get the current cursor position including html tags. My code :
var offset = document.getSelection().focusOffset;
Offset is returning 5 (full text from the last tag) but i need it to handle html tags. The expected return value is 40. The code has to work with all recents browsers.
(i also checked this : window.getSelection() offset with HTML tags? but it doesn't answer my question).
Any ideas ?
Another way to do it is by adding a temporary marker in the DOM and calculating the offset from this marker. The algorithm looks for the HTML serialization of the marker (its outerHTML) within the inner serialization (the innerHTML) of the div of interest. Repeated text is not a problem with this solution.
For this to work, the marker's serialization must be unique within its div. You cannot control what users type into a field but you can control what you put into the DOM so this should not be difficult to achieve. In my example, the marker is made unique statically: by choosing a class name unlikely to cause a clash ahead of time. It would also be possible to do it dynamically, by checking the DOM and changing the class until it is unique.
I have a fiddle for it (derived from Alvaro Montoro's own fiddle). The main part is:
function getOffset() {
if ($("." + unique).length)
throw new Error("marker present in document; or the unique class is not unique");
// We could also use rangy.getSelection() but there's no reason here to do this.
var sel = document.getSelection();
if (!sel.rangeCount)
return; // No ranges.
if (!sel.isCollapsed)
return; // We work only with collapsed selections.
if (sel.rangeCount > 1)
throw new Error("can't handle multiple ranges");
var range = sel.getRangeAt(0);
var saved = rangy.serializeSelection();
// See comment below.
$mydiv[0].normalize();
range.insertNode($marker[0]);
var offset = $mydiv.html().indexOf($marker[0].outerHTML);
$marker.remove();
// Normalizing before and after ensures that the DOM is in the same shape before
// and after the insertion and removal of the marker.
$mydiv[0].normalize();
rangy.deserializeSelection(saved);
return offset;
}
As you can see, the code has to compensate for the addition and removal of the marker into the DOM because this causes the current selection to get lost:
Rangy is used to save the selection and restore it afterwards. Note that the save and restore could be done with something lighter than Rangy but I did not want to load the answer with minutia. If you decide to use Rangy for this task, please read the documentation because it is possible to optimize the serialization and deserialization.
For Rangy to work, the DOM must be in exactly the same state before and after the save. This is why normalize() is called before we add the marker and after we remove it. What this does is merge immediately adjacent text nodes into a single text node. The issue is that adding a marker to the DOM can cause a text node to be broken into two new text nodes. This causes the selection to be lost and, if not undone with a normalization, would cause Rangy to be unable to restore the selection. Again, something lighter than calling normalize could do the trick but I did not want to load the answer with minutia.
EDIT: This is an old answer that doesn't work for OP's requirement of having nodes with the same text. But it's cleaner and lighter if you don't have that requirement.
Here is one option that you can use and that works in all major browsers:
Get the offset of the caret within its node (document.getSelection().anchorOffset)
Get the text of the node in which the caret is located (document.getSelection().anchorNode.data)
Get the offset of that text within #mydiv by using indexOf()
Add the values obtained in 1 and 3, to get the offset of the caret within the div.
The code would look like this for your particular case:
var offset = document.getSelection().anchorOffset;
var text = document.getSelection().anchorNode.data;
var textOffset = $("#mydiv").html().indexOf( text );
offsetCaret = textOffset + offset;
You can see a working demo on this JSFiddle (view the console to see the results).
And a more generic version of the function (that allows to pass the div as a parameter, so it can be used with different contenteditable) on this other JSFiddle:
function getCaretHTMLOffset(obj) {
var offset = document.getSelection().anchorOffset;
var text = document.getSelection().anchorNode.data;
var textOffset = obj.innerHTML.indexOf( text );
return textOffset + offset;
}
About this answer
It will work in all recent browsers as requested (tested on Chrome 42, Firefox 37, and Explorer 11).
It is short and light, and doesn't require any external library (not even jQuery)
Issue: If you have different nodes with the same text, it may return the offset of the first occurrence instead of the real position of the caret.
NOTE: This solution works even in nodes with repeated text, but it detects html entities (e.g.: ) as only one character.
I came up with a completely different solution based on processing the nodes. It is not as clean as the old answer (see other answer), but it works fine even when there are nodes with the same text (OP's requirement).
This is a description of how it works:
Create a stack with all the parent elements of the node in which the caret is located.
While the stack is not empty, traverse the nodes of the containing element (initially the content editable div).
If the node is not the same one at the top of the stack, add its size to the offset.
If the node is the same as the one at the top of the stack: pop it from the stack, go to step 2.
The code is like this:
function getCaretOffset(contentEditableDiv) {
// read the node in which the caret is and store it in a stack
var aux = document.getSelection().anchorNode;
var stack = [ aux ];
// add the parents to the stack until we get to the content editable div
while ($(aux).parent()[0] != contentEditableDiv) { aux = $(aux).parent()[0]; stack.push(aux); }
// traverse the contents of the editable div until we reach the one with the caret
var offset = 0;
var currObj = contentEditableDiv;
var children = $(currObj).contents();
while (stack.length) {
// add the lengths of the previous "siblings" to the offset
for (var x = 0; x < children.length; x++) {
if (children[x] == stack[stack.length-1]) {
// if the node is not a text node, then add the size of the opening tag
if (children[x].nodeType != 3) { offset += $(children[x])[0].outerHTML.indexOf(">") + 1; }
break;
} else {
if (children[x].nodeType == 3) {
// if it's a text node, add it's size to the offset
offset += children[x].length;
} else {
// if it's a tag node, add it's size + the size of the tags
offset += $(children[x])[0].outerHTML.length;
}
}
}
// move to a more inner container
currObj = stack.pop();
children = $(currObj).contents();
}
// finally add the offset within the last node
offset += document.getSelection().anchorOffset;
return offset;
}
You can see a working demo on this JSFiddle.
About this answer:
It works in all major browsers.
It is light and doesn't require external libraries (apart from jQuery)
It has an issue: html entities like are counted as one character only.
I would like to be able to convert the current mouse position to a range, in CKEditor in particular.
The CKEditor provides an API for setting the cursor according to a range:
var ranges = new CKEDITOR.dom.range( editor.document );
editor.getSelection().selectRanges( [ ranges ] );
Since CKEditor provides this API, the problem may be simplified by removing this requirement and just find a way to produce the range from the mouse coordinates over a div containing various HTML elements.
However, this is not the same as converting a mouse coordinate into the cursor position in a textarea since textareas have fixed column widths and row heights where the CKEditor renders HTML through an iframe.
Based on this, it looks like the range may be applied to elements.
How would you figure out the start/end range which is closest to the current mouse position?
Edit:
An example of how one might use the ckeditor API to select a range on the mouseup event.
editor.document.on('mouseup', function(e) {
this.focus();
var node = e.data.$.target;
var range = new CKEDITOR.dom.range( this.document );
range.setStart(new CKEDITOR.dom.node(node), 0);
range.collapse();
var ranges = [];
ranges.push(range);
this.getSelection().selectRanges( ranges );
});
The problem with the above example is that the event target node (e.data.$.target) is only firing for nodes such as HTML, BODY, or IMG but not for text nodes. Even if it did, these nodes represent chunks of text which wouldn't support setting the cursor to the position of the mouse within that chunk of text.
What you're trying to do is really hard in a browser. I'm not familar with ckeditor in particular, but regular javascript allows you to select text using a range so I don't think it's adding anything special. You have to find the browser element that contains the click, then find the character within the element that was clicked.
Detecting the browser element is the easy bit: you need to either register your handler on every element, or use the event's target field. There is lots of info on this out there, ask a more specific question on stackoverflow if that's what you're having trouble with.
Once you have the element you need to find out which character within the element was clicked, then create an appropriate range to put the cursor there. As the post you linked to stated, browser variations make this really hard. This page is a bit dated, but has a good discussion of ranges: http://www.quirksmode.org/dom/range_intro.html
Ranges can't tell you their positions on the page, so you'll have to use another technique to find out what bit of text was clicked.
I've never seen a complete solution to this in javascript. A few years ago I worked on one but I didn't come up with an answer I was happy with (some really hard edge cases). The approach I used was a horrible hack: insert spans into the text then use them to perform binary search until you find the smallest possible span containing the mouse click. Spans don't change the layout, so you can use the span's position_x/y properties to find out they contain the click.
E.g. suppose you have the following text in a node:
<p>Here is some paragraph text.</p>
We know the click was somewhere in this paragraph. Split the paragraph in half with a span:
<p><span>Here is some p</span>aragraph text.</p>
If the span contains the click coordinates, continue binary search in that half, otherwise search the second half.
This works great for single lines, but if the text spans multiple lines you have to first find line breaks, or the spans can overlap. You also have to work out what to do when the click wasn't on any text but was in the element --- past the end of the last line in a paragraph for example.
Since I worked on this browsers have got a lot faster. They're probably fast enough now to add s around each character, then around each two characters etc to create a binary tree which is easy to search. You could try this approach - it would make it much easier to work out which line you're working on.
TL;DR this is a really hard problem and if there is an answer, it might not be worth your time to come up with it.
Sorry for bumping an old thread, but I wanted to post this here in case anyone else stumbles across this question, as there is very little information on this. I just had to write a function that does this for an Outlook for web userscript, because they override the default drag-n-drop functionality and break it in the compose box. This is the solution I came up with:
function rangeFromCoord(x, y) {
const closest = {
offset: 0,
xDistance: Infinity,
yDistance: Infinity,
};
const {
minOffset,
maxOffset,
element,
} = (() => {
const range = document.createRange();
range.selectNodeContents(document.elementFromPoint(x, y));
return {
element: range.startContainer,
minOffset: range.startOffset,
maxOffset: range.endOffset,
};
})();
for(let i = minOffset; i <= maxOffset; i++) {
const range = document.createRange();
range.setStart(element, i);
range.setEnd(element, i);
const marker = document.createElement("span");
marker.style.width = "0";
marker.style.height = "0";
marker.style.position = "absolute";
marker.style.overflow = "hidden";
range.insertNode(marker);
const rect = marker.getBoundingClientRect();
const distX = Math.abs(x - rect.left);
const distY = Math.abs(y - rect.top);
marker.remove();
if(closest.yDistance > distY) {
closest.offset = i;
closest.xDistance = distX;
closest.yDistance = distY;
} else if(closest.yDistance === distY) {
if(closest.xDistance > distX) {
closest.offset = i;
closest.xDistance = distX;
closest.yDistance = distY;
}
}
}
const range = document.createRange();
range.setStart(element, closest.offset);
range.setEnd(element, closest.offset);
return range;
}
All you do is pass in the client coordinates, and the function will automatically select the most specific element at that position. It will use that selection to get the parent element used by the browser (most notably contenteditable elements), as well as the maximum and minimum offsets. It will then proceed, iterating through the offsets, placing marker span elements with position: absolute; width: 0; height: 0; overflow: hidden; at each offset to probe their position, removing them, and checking distance. As per most text editors, it will first get as close as it can on the Y coordinate, and then move in on the X coordinate. Once it finds the closest position, it will create a new selection and return it.
There are two ways of doing this, just like every WYSIWYG does.
First:
- you give up because it is too hard and it will end up to be a browser killer;
Second:
- you try to parse the text and put it in the exact place in a semitransparent textarea or div above the original, but here we have two problems:
1) how would you parse the dynamic chunks of data to get only the text and to be sure you map it over the exact position of the actual content
2) how would you solve the update to parse for every darn character you type or every action you do in the editor.
In the end this is just a "A brutal odyssey to the dark side of the DOM tree", but if you choose the second way, than the code from your post will work like a charm.
I was working on a similar task to allow TinyMCE (inline mode) to initialize with a caret placed in mouse click position. The following code works in the latest Firefox and Chrome, at least:
let contentElem = $('#editorContentRootElem');
let editorConfig = { inline: true, forced_root_block: false };
let onFirstFocus = () => {
contentElem.off('click focus', onFirstFocus);
setTimeout(() => {
let uniqueId = 'uniqueCaretId';
let range = document.getSelection().getRangeAt(0);
let caret = document.createElement("span");
range.surroundContents(caret);
caret.outerHTML = `<span id="${uniqueId}" contenteditable="false"></span>`;
editorConfig.setup = (editor) => {
this.editor = editor;
editor.on('init', () => {
var caret = $('#' + uniqueId)[0];
if (!caret) return;
editor.selection.select(caret);
editor.selection.collapse(false);
caret.parentNode.removeChild(caret);
});
};
tinymce.init(editorConfig);
}, 0); // after redraw
}; // onFirstFocus
contentElem.on('click focus', onFirstFocus);
Explanation
It seems that after mouse click/focus event and redraw (setTimeout ms 0) document.getSelection().getRangeAt(0) returns valid cursor range. We can use it for any purpose. TinyMCE moves caret to start on initialization, so I create special span 'caret' element at current range start and later force editor to select it, then remove it.
So I feel like I'm a bit out of my league here. But here's what I want to do.
Basically, I want to have a user select part of text within in a paragraph (which may contain many elemnts (i.e. <span> and <a>) to return the value of the id attribute of that paragraph. Here's what I thinking.
function getParaID() //function will be called using a mouseUp event
{
var selObj = window.getSelection();
var selRange = selObj.getRangeAt(0); //btw can anyone explain what this zero means
var paraElement = selRange.commonAncestorContainer;
var paraID = paraElement.getAttribute;
return paraID;
}
What do you think? Am I close?
The selection range's commonAncestorContainer property may be a reference to a text node, or a <span> or <a> element or <body> element, or whatever else you may have in your page. That being the case, you need to work up the DOM tree to find the containing <p> element, if one exists. You also need to be aware that IE < 9 does not support window.getSelection() or DOM Range, although it is possible to do what you want quite easily in IE < 9. Here's some code that will work in all major browsers, including IE 6:
jsFiddle: http://jsfiddle.net/44Juf/
Code:
function getContainingP(node) {
while (node) {
if (node.nodeType == 1 && node.tagName.toLowerCase() == "p") {
return node;
}
node = node.parentNode;
}
}
function getParaID() {
var p;
if (window.getSelection) {
var selObj = window.getSelection();
if (selObj.rangeCount > 0) {
var selRange = selObj.getRangeAt(0);
p = getContainingP(selRange.commonAncestorContainer);
}
} else if (document.selection && document.selection.type != "Control") {
p = getContainingP(document.selection.createRange().parentElement());
}
return p ? p.id : null;
}
Regarding the 0 passed to getRangeAt(), that is indicating which selected range you want. Firefox supports multiple selected ranges: if you make a selection and then hold down Ctrl and make another selection, you will see you now have two discontinous ranges selected, which can be accessed via getRangeAt(0) and getRangeAt(1). Also in Firefox, selecting a column of cells in a table creates a separate range for each selected cell. The number of selected ranges can be obtained using the rangeCount property of the selection. No other major browser supports multiple selected ranges.
You're quite close. If all you want is the id of the parent element, then you should replace your paraElement.getAttribute with paraElement.id, like:
var paraID = paraElement.id;
Regarding the parameter to getRangeAt(), it is specifying the index of the selection range to return, and it's only really relevant to controls that allow discontinuous selections. For instance, a select box in which the user can use ctrl + click to select several arbitrary groups of rows simultaneously. In such a case you could use the parameter to step from one selected region to the next. But for highlighting text within a paragraph you should never have a discontinuous selection and thus can always pass 0. In essence it means that you're asking for "the first selected region".
Also note that if your interface allows the user's selection to span multiple paragraphs then your commonAncestorContainer may not be a paragraph, it might also be whatever element it is that contains all of your paragraph tags. So you should be prepared to handle that case.
Edit:
After playing with this a bit, here is my suggestion: http://jsfiddle.net/vCsZH/
Basically, instead of relying on commonAncestorContainer this code applies a mouseDown and a mouseUp listener to each paragraph (in addition to the one already applied to the top-level container). The listeners will, in essence, record the paragraphs that the selection range starts and ends at, making it much simpler to reliably work out which paragraph(s) are selected.
If ever there was a case in favor of using dynamic event binding through a framework like jQuery, this is it.