如何使用HTML标签包装跨边界DOM选择范围?

19

我现在通过 s = window.getSelection()range = s.getRangeAt(0) 来获取用户选中的文本 (浏览器实现除外)。只要在<p>标签内进行选择,我就可以轻松调用range.surroundContents(document.createElement("em"))让所选文本被包裹<em>标签中。

然而,在这个例子中,

<p>This is the Foo paragraph.</p>
<p>This is the Bar paragraph.</p>
<p>This is the Baz paragraph.</p>

当用户从FooBaz选择文本时,我无法调用range.surroundContents:Firefox会失败并显示The boundary-points of a range does not meet specific requirements." code: "1,因为所选内容不是有效的HTML。

在这种情况下,我想以某种方式在DOM中获取以下状态:

<p>This is the <em>Foo paragraph.</em></p>
<p><em>This is the Bar paragraph.</em></p>
<p><em>This is the Baz</em> paragraph.</p>

有什么想法吗?


补充一下:我一直在尝试使用RangeAPI,但我无法找到一个简单明了的方法来实现这个结果。

var r = document.createRange();
r.setStart(range.startContainer, range.startOffset);
r.setEnd(range.endContainer, range.endOffset+40);
selection.addRange(r);

我最终可以通过重新定位偏移量来实现某些东西的黑客方式,但仅适用于“开始”和“结束”容器!(即在这种情况下Bar段落,我该如何包装它?)


3
在JavaScript中最让人沮丧的事情之一,至少我是这么认为的...希望你能找到一个好的答案! - Zoidberg
是的,我确认。这真是个噩梦。 - user52154
+1 为向权力说真话而加分 - Don
1
在这里使用“Rangy”库查看答案:https://dev59.com/qFbTa4cB1Zd3GeqP_YEG - Tolan
仍然面临这个噩梦 :( - Hemant_Negi
3个回答

12

你是否尝试过以下方法(实际上这是W3C规范中surroundContents应该执行的描述):

var wrappingNode = document.createElement("div");
wrappingNode.appendChild(range.extractContents());
range.insertNode(wrappingNode);

感谢 @nlazarov 的建议。我已将其提取为一个独立的CommonJS模块“ wrap-range”:https://github.com/webmodules/wrap-range - TooTallNate
但是 OP 要求“用 HTML 标记包装跨边界 DOM 选择范围”。 - Hemant_Negi
@nlazarov - 天啊,您真是个救星!我被 surroundContents() 函数卡了一整天,但就是无法解决。 - davidanderle

3

我目前正在开发一个内联编辑器,我已经编写了一个函数,可以像execCommand一样正确地将跨元素范围包装在任何类型的元素中。

function surroundSelection(elementType) {
    function getAllDescendants (node, callback) {

        for (var i = 0; i < node.childNodes.length; i++) {
            var child = node.childNodes[i];
            getAllDescendants(child, callback);
            callback(child);
        }

    }

    function glueSplitElements (firstEl, secondEl){

        var done = false,
            result = [];

        if(firstEl === undefined || secondEl === undefined){
            return false;
        }

        if(firstEl.nodeName === secondEl.nodeName){
            result.push([firstEl, secondEl]);

            while(!done){
                firstEl = firstEl.childNodes[firstEl.childNodes.length - 1];
                secondEl = secondEl.childNodes[0];

                if(firstEl === undefined || secondEl === undefined){
                    break;
                }

                if(firstEl.nodeName !== secondEl.nodeName){
                    done = true;
                } else {
                    result.push([firstEl, secondEl]);
                }
            }
        }

        for(var i = result.length - 1; i >= 0; i--){
            var elements = result[i];
            while(elements[1].childNodes.length > 0){
                elements[0].appendChild(elements[1].childNodes[0]);
            }
            elements[1].parentNode.removeChild(elements[1]);
        }

    }

    // abort in case the given elemenType doesn't exist.
    try {
        document.createElement(elementType);
    } catch (e){
        return false;
    }

    var selection = getSelection();

    if(selection.rangeCount > 0){
        var range = selection.getRangeAt(0),
            rangeContents = range.extractContents(),
            nodesInRange  = rangeContents.childNodes,
            nodesToWrap   = [];

        for(var i = 0; i < nodesInRange.length; i++){
            if(nodesInRange[i].nodeName.toLowerCase() === "#text"){
                nodesToWrap.push(nodesInRange[i]);
            } else {
                getAllDescendants(nodesInRange[i], function(child){
                    if(child.nodeName.toLowerCase() === "#text"){
                        nodesToWrap.push(child);
                    }
                });
            }
        };


        for(var i = 0; i < nodesToWrap.length; i++){
            var child = nodesToWrap[i],
                wrap = document.createElement(elementType);

            if(child.nodeValue.replace(/(\s|\n|\t)/g, "").length !== 0){
                child.parentNode.insertBefore(wrap, child);
                wrap.appendChild(child);
            } else {
                wrap = null;
            }
        }

        var firstChild = rangeContents.childNodes[0];
        var lastChild = rangeContents.childNodes[rangeContents.childNodes.length - 1];

        range.insertNode(rangeContents);

        glueSplitElements(firstChild.previousSibling, firstChild);
        glueSplitElements(lastChild, lastChild.nextSibling);

        rangeContents = null;
    }
};

这是一个涉及复杂HTML的JSFiddle演示: http://jsfiddle.net/mjf9K/1/。请注意,我直接从我的应用程序中提取了这些内容。我使用一些帮助程序来正确恢复范围到原始的选择等等。这些未包含在内。

当lastElement是&nbsp;时,它遗憾地失败了。 - mayankcpdixit

1

当您将contentEditable=true属性添加到这些段落的父元素中时,选择任何文本,即使跨越段落,然后进行调用。

document.execCommand('italic', false, null);

最后,如果需要的话,将contentEditable属性设置回false。

顺便说一下,这也适用于IE,只是为了进入可编辑模式,我认为它被称为设计模式或其他什么,可以在谷歌上搜索一下。


非常好的答案,但是我已经尝试过这个方法了,对我来说并不太适用,因为我需要访问<em>位置/偏移量以进行其他操作。除非您知道如何使用contentEditable获取它们?无论如何感谢您。 - user52154
如果您不是非常热衷于使用<em>标签,但可以接受<i>标签来完成您想要做的事情,那么以下内容将起作用:document.execCommand('styleWithCSS', false, false);document.execCommand('italic', false, null); - Murali VP

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