cronoklee
cronoklee

Reputation: 6722

JS: Get array of all selected nodes in contentEditable div

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

Answers (4)

1110101001
1110101001

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

payam jabbari
payam jabbari

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

Tim Down
Tim Down

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

Daniel Mendel
Daniel Mendel

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

Related Questions