Reputation: 6722
Hi I've been working with contentEditable for a while now and I think I have a pretty good handle on it. One thing that's evading me is how to get an array of references to all nodes that are partially or fully within the user's selection. Anyone got an idea?
Here's something to start from:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<script type="text/javascript">
function getSelectedNodes(){
var sel = window.getSelection();
try{var frag=sel.getRangeAt(0).cloneContents()}catch(e){return(false);}
var tempspan = document.createElement("span");
tempspan.appendChild(frag);
var selnodes = Array() //<<- how do I fill this array??
var output = ''
for(i in selnodes){
output += "A "+selnodes[i].tagName+" was found\n"
//do something cool with each element here...
}
return(output)
}
</script>
</head>
<body contentEditable="true" onkeypress="return(keypress(event))">
<div>This <strong>div</strong> is <em>content</em> <span class='red'>editable</span> and has a couple of <em><strong>child nodes</strong></em> within it</div>
<br />
<br />
<a href="#" onmouseover="alert(getSelectedNodes())">hover here</a>
</body>
</html>
Upvotes: 18
Views: 12089
Reputation: 5097
Tim Down's answer is close, but it ignores startOffset and endOffset which can lead to weird behavior in some cases. His Rangy library handles this correctly, but for those who don't want an entire dependency here's just the relevant code extracted out
function isCharacterDataNode(node) {
var t = node.nodeType;
return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment
}
function getClosestAncestorIn(node, ancestor, selfIsAncestor) {
var p, n = selfIsAncestor ? node : node.parentNode;
while (n) {
p = n.parentNode;
if (p === ancestor) {
return n;
}
n = p;
}
return null;
}
function RangeIterator(range, clonePartiallySelectedTextNodes) {
this.range = range;
this.clonePartiallySelectedTextNodes = clonePartiallySelectedTextNodes;
if (!range.collapsed) {
this.sc = range.startContainer;
this.so = range.startOffset;
this.ec = range.endContainer;
this.eo = range.endOffset;
var root = range.commonAncestorContainer;
if (this.sc === this.ec && isCharacterDataNode(this.sc)) {
this.isSingleCharacterDataNode = true;
this._first = this._last = this._next = this.sc;
} else {
this._first = this._next = (this.sc === root && !isCharacterDataNode(this.sc)) ?
this.sc.childNodes[this.so] : getClosestAncestorIn(this.sc, root, true);
this._last = (this.ec === root && !isCharacterDataNode(this.ec)) ?
this.ec.childNodes[this.eo - 1] : getClosestAncestorIn(this.ec, root, true);
}
console.log("RangeIterator first and last", this._first, this._last);
}
}
RangeIterator.prototype = {
_current: null,
_next: null,
_first: null,
_last: null,
isSingleCharacterDataNode: false,
reset: function() {
this._current = null;
this._next = this._first;
},
hasNext: function() {
return !!this._next;
},
next: function() {
// Move to next node
var current = this._current = this._next;
if (current) {
this._next = (current !== this._last) ? current.nextSibling : null;
}
return current;
},
};
Another way to solve this is to make use of range.intersectsNode
function and just walk over the LCA container finding everything that intersects, as mentioned by Tim in a different answer
var selcRange = window.getSelection().getRangeAt(0)
var containerElement = selcRange.commonAncestorContainer;
if (containerElement.nodeType != 1) {
containerElement = containerElement.parentNode;
}
var walk = document.createTreeWalker(containerElement, NodeFilter.SHOW_ALL,
{ acceptNode: function(node) {
// Logic to determine whether to accept, reject or skip node
// In this case, only accept nodes that have content
// other than whitespace
return selcRange.intersectsNode(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
}
}, false);
var n = walk.nextNode();
while (n) {
s.push(n);
n = walk.nextNode();
}
console.log(s)
Upvotes: 0
Reputation: 19
Below Code is sample to solve your problem, below code return all selected node that in range
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>payam jabbari</title>
<script src="http://code.jquery.com/jquery-2.0.2.min.js" type="text/javascript"></script>
<script type="text/javascript">
$(document).ready(function(){
var startNode = $('p.first').contents().get(0);
var endNode = $('span.second').contents().get(0);
var range = document.createRange();
range.setStart(startNode, 0);
range.setEnd(endNode, 5);
var selection = document.getSelection();
selection.addRange(range);
// below code return all nodes in selection range. this code work in all browser
var nodes = range.cloneContents().querySelectorAll("*");
for(var i=0;i<nodes.length;i++)
{
alert(nodes[i].innerHTML);
}
});
</script>
</head>
<body>
<div>
<p class="first">Even a week ago, the idea of a Russian military intervention in Ukraine seemed far-fetched if not totally alarmist. But the arrival of Russian troops in Crimea over the weekend has shown that he is not averse to reckless adventures, even ones that offer little gain. In the coming days and weeks</p>
<ol>
<li>China says military will respond to provocations.</li>
<li >This Man Has Served 20 <span class="second"> Years—and May Die—in </span> Prison for Marijuana.</li>
<li>At White House, Israel's Netanyahu pushes back against Obama diplomacy.</li>
</ol>
</div>
</body>
</html>
Upvotes: 0
Reputation: 324627
Here's a version that gives you the actual selected and partially selected nodes rather than clones. Alternatively you could use my Rangy library, which has a getNodes()
method of its Range objects and works in IE < 9.
function nextNode(node) {
if (node.hasChildNodes()) {
return node.firstChild;
} else {
while (node && !node.nextSibling) {
node = node.parentNode;
}
if (!node) {
return null;
}
return node.nextSibling;
}
}
function getRangeSelectedNodes(range) {
var node = range.startContainer;
var endNode = range.endContainer;
// Special case for a range that is contained within a single node
if (node == endNode) {
return [node];
}
// Iterate nodes until we hit the end container
var rangeNodes = [];
while (node && node != endNode) {
rangeNodes.push( node = nextNode(node) );
}
// Add partially selected nodes at the start of the range
node = range.startContainer;
while (node && node != range.commonAncestorContainer) {
rangeNodes.unshift(node);
node = node.parentNode;
}
return rangeNodes;
}
function getSelectedNodes() {
if (window.getSelection) {
var sel = window.getSelection();
if (!sel.isCollapsed) {
return getRangeSelectedNodes(sel.getRangeAt(0));
}
}
return [];
}
Upvotes: 46
Reputation: 10003
You're so close! When you append the Document Fragment
to the temporary span
element, you've turned them into a manageable group, accessible through the trusty childNodes
array.
var selnodes = tempspan.childNodes;
Additionally, you're setting yourself up for some trouble with that for(i in selnodes)
loop, which would return the elements in the array, PLUS the length
property, and the __proto__
property, and any other properties the object may have.
You should really only use those kinds of for
loops when looping over the properties in an object, and then always with if (obj.hasOwnProperty[i])
to filter out properties inherited from the prototype.
When looping through arrays, use:
for(var i=0,u=selnodes.length;i<u;i++)
Finally, once you load that array, you'll actually need to check each element to see if it's a DOM node or a Text node before you can handle it. We can do that by checking to see if it supports the tagName
property.
if (typeof selnodes[i].tagName !== 'undefined')
Here's the whole thing:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<script type="text/javascript">
function getSelectedNodes(){
var sel = window.getSelection();
try{var frag=sel.getRangeAt(0).cloneContents()}catch(e){return(false);}
var tempspan = document.createElement("span");
tempspan.appendChild(frag);
console.log(tempspan);
window.selnodes = tempspan.childNodes;
var output = ''
for(var i=0, u=selnodes.length;i<u;i++){
if (typeof selnodes[i].tagName !== 'undefined'){
output += "A "+selnodes[i].tagName+" was found\n"
}
else output += "Some text was found: '"+selnodes[i].textContent+"'\n";
//do something cool with each element here...
}
return(output)
}
</script>
</head>
<body contentEditable="true" onkeypress="return(keypress(event))">
<div>This <strong>div</strong> is <em>content</em> <span class='red'>editable</span> and has a couple of <em><strong>child nodes</strong></em> within it</div>
<br />
<br />
<a href="#" onmouseover="alert(getSelectedNodes())">hover here</a>
</body>
</html>
Upvotes: 2