最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - How to wrap with HTML tags a cross-boundary DOM selection range? - Stack Overflow

programmeradmin3浏览0评论

Right now I'm capturing users' text selections through s = window.getSelection() and range = s.getRangeAt(0) (browser's impls aside). Whenever a selection within a <p> is made, I can easily call range.surroundContents(document.createElement("em")) to have the selected text wrapped with an <em> tag.

In this example, however,

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

when a user makes a text selection from Foo to Baz, I cannot call range.surroundContents: Firefox fails with The boundary-points of a range does not meet specific requirements." code: "1 because the selection is not valid HTML.

In that case, I'd like to somehow obtain the following state in the 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>

Any ideas?


FYI: I've been trying with the Range API but I can't see a straightforward way of achieving that result. With

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

I can eventually hack something by repositioning the offsets, but only for the 'start' and 'end' containers! (i.e. in this case the Bar paragraph, how do I wrap it?)

Right now I'm capturing users' text selections through s = window.getSelection() and range = s.getRangeAt(0) (browser's impls aside). Whenever a selection within a <p> is made, I can easily call range.surroundContents(document.createElement("em")) to have the selected text wrapped with an <em> tag.

In this example, however,

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

when a user makes a text selection from Foo to Baz, I cannot call range.surroundContents: Firefox fails with The boundary-points of a range does not meet specific requirements." code: "1 because the selection is not valid HTML.

In that case, I'd like to somehow obtain the following state in the 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>

Any ideas?


FYI: I've been trying with the Range API but I can't see a straightforward way of achieving that result. With

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

I can eventually hack something by repositioning the offsets, but only for the 'start' and 'end' containers! (i.e. in this case the Bar paragraph, how do I wrap it?)

Share Improve this question asked Nov 13, 2009 at 18:18 user52154user52154 5
  • 3 One of the most frustrating things to do in javascript, at least I found that... I hope you find a good answer! – Zoidberg Commented Nov 13, 2009 at 18:23
  • Yes, I confirm. This is a nightmare. – user52154 Commented Nov 17, 2009 at 13:57
  • +1 for speaking truth to power – Don Commented Nov 18, 2009 at 5:35
  • 1 See the answer using the 'Rangy' library here: stackoverflow.com/questions/5765381/… – Tolan Commented Dec 1, 2011 at 16:28
  • Still facing this nightmare :( – Hemant_Negi Commented Sep 18, 2017 at 2:36
Add a comment  | 

3 Answers 3

Reset to default 12

have you tried the following approach (which is actually the description in the W3C spec of what surroundContents should do):

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

I'm currently working on an inline editor and I've written a function that can properly wrap a cross-element range with any type of element like the execCommand does.

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

Here's a JSFiddle with some complex HTML as demo: http://jsfiddle.net/mjf9K/1/. Please note that I took this straight out of my application. I use a few helpers to correctly restore the range to the original selection etc. These are not included.

That is when you add contentEditable=true attribute to the parent of those paragraphs, select any text, even across paragraphs, then make the call

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

and finally if desired set contentEditable attribute back to false.

Btw, this works on IE too, except that to enter editable mode I think it is called designMode or something, google for it.

发布评论

评论列表(0)

  1. 暂无评论