JS:获取contentEditable div中所有选定节点的数组

17

嗨,我已经使用contentEditable有一段时间了,我认为我对它有很好的掌握。唯一让我困扰的是如何获取所有部分或全部包含在用户选择范围内的节点的引用数组。有人有什么想法吗?

以下是一个可以开始工作的示例:

<!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>
4个回答

43
这里有一个版本,它可以给你实际选择的和部分选择的节点而不是克隆。或者你可以使用我的Rangy库,它对于其Range对象具有一个getNodes()方法并且在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 [];
}

Tim,我该如何修改数组中的节点?我想通过使用 outerHTML 来合并两个特定的 p 节点(删除第一个节点的 </p> 和第二个节点的 <p>),但它不起作用。 - horse
2
这太棒了!!!我已经抓狂了,因为我必须使用cloneContents(),这会丢失对DOM的引用。这是金子般的建议,Tim。 - msqar
这里有一个边缘情况,如果你在像<b> text 1 <br> text 2 </b>这样的结构中,并且突出显示了text1,则startContainer是文本,而endContainerb,而next永远不会返回b,因此它最终会返回所有元素。我认为你必须考虑endOffsetstartOffset。Rangy可以正确处理这个问题(考虑到偏移量),所以如果你不介意,我已经添加了一个答案,其中包含从Rangy中提取的自包含代码。 - 1110101001

2

你已经非常接近了!当你将 Document Fragment 追加到临时的 span 元素中时,它们就成为了一个可管理的组,可以通过可靠的 childNodes 数组进行访问。

    var selnodes = tempspan.childNodes;

此外,那个 for(i in selnodes) 循环会给你带来一些麻烦,它会返回数组中的元素以及 length 属性、__proto__ 属性和对象可能具有的其他属性。

在循环遍历对象的属性时应该只使用这种类型的 for 循环,然后始终使用 if (obj.hasOwnProperty[i]) 来过滤从原型继承的属性。

循环遍历数组时,请使用:

    for(var i=0,u=selnodes.length;i<u;i++)

最后,一旦您加载了该数组,实际上需要检查每个元素,以查看它是DOM节点还是文本节点,然后再进行处理。我们可以通过检查是否支持tagName属性来实现这一点。

    if (typeof selnodes[i].tagName !== 'undefined')

以下是全部内容:

<!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>

哇,我真的很接近了!childNodes是否真的返回DOM实际节点的数组?我无法在数组内容上使用标准属性。例如:selnodes[i].innerHTML ='P:'+selnodes[i].innerHTML - cronoklee
1
啊 - 所以这样返回的是tempspan中的节点。我该如何引用原始节点,以便更改其内容? - cronoklee
@cronoklee 您可以访问 range.commonAncestorContainer 节点,并迭代其子节点,直到找到 range.startContainerrange.endContainer。https://developer.mozilla.org/en-US/docs/Web/API/Range - Steven

0

Tim Down的回答很接近,但它忽略了startOffset和endOffset,这可能会在某些情况下导致奇怪的行为。他的Rangy库可以正确处理此问题,但对于那些不想要整个依赖项的人,这里只提取出相关代码。

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;
    },
};

解决这个问题的另一种方法是利用range.intersectsNode函数,只需遍历LCA容器找到所有相交的内容,正如Tim在不同的答案中提到的。

        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)

0
以下代码是解决您问题的示例,下面的代码返回所有在范围内的选定节点。
<!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>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接