Get selected text and selected nodes on a page?
You are in for a bumpy ride, but this is quite possible. The main problem is that IE and W3C expose completely different interfaces to selections so if you want cross browser functionality then you basically have to write the whole thing twice. Also, some basic functionality is missing from both interfaces.
Mozilla developer connection has the story on W3C selections. Microsoft have their system documented on MSDN. I recommend starting at PPK's introduction to ranges.
Here are some basic functions that I believe work:
// selection objects will differ between browsers
function getSelection () {
return ( msie )
? document.selection
: ( window.getSelection || document.getSelection )();
}
// range objects will differ between browsers
function getRange () {
return ( msie )
? getSelection().createRange()
: getSelection().getRangeAt( 0 )
}
// abstract getting a parent container from a range
function parentContainer ( range ) {
return ( msie )
? range.parentElement()
: range.commonAncestorContainer;
}
My Rangy library will get your part of the way there by unifying the different APIs in IE < 9 and all other major browsers, and by providing a getNodes()
function on its Range objects:
function getSelectedNodes() {
var selectedNodes = [];
var sel = rangy.getSelection();
for (var i = 0; i < sel.rangeCount; ++i) {
selectedNodes = selectedNodes.concat( sel.getRangeAt(i).getNodes() );
}
return selectedNodes;
}
Getting the selected text is pretty easy in all browsers. In Rangy it's just
var selectedText = rangy.getSelection().toString();
Without Rangy:
function getSelectedText() {
var sel, text = "";
if (window.getSelection) {
text = "" + window.getSelection();
} else if ( (sel = document.selection) && sel.type == "Text") {
text = sel.createRange().text;
}
return text;
}
As for the character offsets, you can do something like this for any node node
in the selection. Note this does not necessarily represent the visible text in the document because it takes no account of collapsed spaces, text hidden via CSS, text positioned outside the normal document flow via CSS, line breaks implied by <br>
and block elements, plus other subtleties.
var sel = rangy.getSelection();
var selRange = sel.getRangeAt(0);
var rangePrecedingNode = rangy.createRange();
rangePrecedingNode.setStart(selRange.startContainer, selRange.startOffset);
rangePrecedingNode.setEndBefore(node);
var startIndex = rangePrecedingNode.toString().length;
rangePrecedingNode.setEndAfter(node);
var endIndex = rangePrecedingNode.toString().length;
alert(startIndex + ", " + endIndex);
This returns the selected nodes as I understand it: When I have
<p> ... </p><p> ... </p><p> ... </p><p> ... </p><p> ... </p>...
<p> ... </p><p> ... </p><p> ... </p><p> ... </p><p> ... </p>
a lot of nodes and I select only a few then I want only these nodes to be in the list.
function getSelectedNodes() {
// from https://developer.mozilla.org/en-US/docs/Web/API/Selection
var selection = window.getSelection();
if (selection.isCollapsed) {
return [];
};
var node1 = selection.anchorNode;
var node2 = selection.focusNode;
var selectionAncestor = get_common_ancestor(node1, node2);
if (selectionAncestor == null) {
return [];
}
return getNodesBetween(selectionAncestor, node1, node2);
}
function get_common_ancestor(a, b)
{
// from http://stackoverflow.com/questions/3960843/how-to-find-the-nearest-common-ancestors-of-two-or-more-nodes
$parentsa = $(a).parents();
$parentsb = $(b).parents();
var found = null;
$parentsa.each(function() {
var thisa = this;
$parentsb.each(function() {
if (thisa == this)
{
found = this;
return false;
}
});
if (found) return false;
});
return found;
}
function isDescendant(parent, child) {
// from http://stackoverflow.com/questions/2234979/how-to-check-in-javascript-if-one-element-is-a-child-of-another
var node = child;
while (node != null) {
if (node == parent) {
return true;
}
node = node.parentNode;
}
return false;
}
function getNodesBetween(rootNode, node1, node2) {
var resultNodes = [];
var isBetweenNodes = false;
for (var i = 0; i < rootNode.childNodes.length; i+= 1) {
if (isDescendant(rootNode.childNodes[i], node1) || isDescendant(rootNode.childNodes[i], node2)) {
if (resultNodes.length == 0) {
isBetweenNodes = true;
} else {
isBetweenNodes = false;
}
resultNodes.push(rootNode.childNodes[i]);
} else if (resultNodes.length == 0) {
} else if (isBetweenNodes) {
resultNodes.push(rootNode.childNodes[i]);
} else {
return resultNodes;
}
};
if (resultNodes.length == 0) {
return [rootNode];
} else if (isDescendant(resultNodes[resultNodes.length - 1], node1) || isDescendant(resultNodes[resultNodes.length - 1], node2)) {
return resultNodes;
} else {
// same child node for both should never happen
return [resultNodes[0]];
}
}
The code should be available at: https://github.com/niccokunzmann/spiele-mit-kindern/blob/gh-pages/javascripts/feedback.js
I posted this answer here because I would have liked to find it here.