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

range - javascript contentEditable - wrap cross-tag selections - Stack Overflow

programmeradmin7浏览0评论

I'm experimenting a bit with contentEditable and encountered this issue: I have the following js snippet

var range = document.getSelection().getRangeAt(0);
var newNode = document.createElement("span");
newNode.className = "customStyle";
range.surroundContents(newNode);

and this HTML fragment:

<ul>
    <li>the <b>only entry</b> of the list</li>
</ul>
<p>Some text here in paragraph</p>

The js code allows to wrap the current selection with a <span> tag.

It works perfectly when the selection includes whole HTML tags (e.g. selecting 'the only entry of') but not, of course, when the selection includes only one of their endings (e.g. selecting from 'entry' to 'Some', both included).

Though I'm aware this problem is not trivial, I'm looking for suggestions about the best approach. Thanks in advance!

I'm experimenting a bit with contentEditable and encountered this issue: I have the following js snippet

var range = document.getSelection().getRangeAt(0);
var newNode = document.createElement("span");
newNode.className = "customStyle";
range.surroundContents(newNode);

and this HTML fragment:

<ul>
    <li>the <b>only entry</b> of the list</li>
</ul>
<p>Some text here in paragraph</p>

The js code allows to wrap the current selection with a <span> tag.

It works perfectly when the selection includes whole HTML tags (e.g. selecting 'the only entry of') but not, of course, when the selection includes only one of their endings (e.g. selecting from 'entry' to 'Some', both included).

Though I'm aware this problem is not trivial, I'm looking for suggestions about the best approach. Thanks in advance!

Share Improve this question asked Apr 14, 2015 at 17:12 ilPittizilPittiz 7561 gold badge10 silver badges25 bronze badges 3
  • Take a look at the answer to this question: stackoverflow./questions/5765381/… – Kris Commented Apr 14, 2015 at 17:24
  • what would you want to happen? you can't end a tag in a different parent than you open the tag... – dandavis Commented Apr 14, 2015 at 17:51
  • Selecting from 'entry' to 'Some' would result into something like <b><span class="customStyle">entry</span></b> <span class="customStyle">of the list</span></li> </ul> <p><span class="customStyle">Some</span></p>. Tim Down's answer and solution would do pretty well! – ilPittiz Commented Apr 15, 2015 at 17:08
Add a ment  | 

1 Answer 1

Reset to default 12

The basic approach if you're only interested in wrapping the text parts is:

  • Get the selection range
  • For each range boundary, if it lies in the middle of a text node, you need to split the text node in two at the boundary and update the range's boundary so that the range stays in place (example code from Rangy)
  • Get all the text nodes within the range (example code)
  • Surround each text node in a <span> element
  • Re-select the range

This is the approach taken by the class applier module of my Rangy library.

I've created an example, mostly using code adapted from Rangy:

function getNextNode(node) {
    var next = node.firstChild;
    if (next) {
        return next;
    }
    while (node) {
        if ( (next = node.nextSibling) ) {
            return next;
        }
        node = node.parentNode;
    }
}

function getNodesInRange(range) {
    var start = range.startContainer;
    var end = range.endContainer;
    var monAncestor = range.monAncestorContainer;
    var nodes = [];
    var node;

    // Walk parent nodes from start to mon ancestor
    for (node = start.parentNode; node; node = node.parentNode) {
        nodes.push(node);
        if (node == monAncestor) {
            break;
        }
    }
    nodes.reverse();

    // Walk children and siblings from start until end is found
    for (node = start; node; node = getNextNode(node)) {
        nodes.push(node);
        if (node == end) {
            break;
        }
    }

    return nodes;
}

function getNodeIndex(node) {
    var i = 0;
    while ( (node = node.previousSibling) ) {
        ++i;
    }
    return i;
}

function insertAfter(node, precedingNode) {
    var nextNode = precedingNode.nextSibling, parent = precedingNode.parentNode;
    if (nextNode) {
        parent.insertBefore(node, nextNode);
    } else {
        parent.appendChild(node);
    }
    return node;
}

// Note that we cannot use splitText() because it is bugridden in IE 9.
function splitDataNode(node, index) {
    var newNode = node.cloneNode(false);
    newNode.deleteData(0, index);
    node.deleteData(index, node.length - index);
    insertAfter(newNode, node);
    return newNode;
}

function isCharacterDataNode(node) {
    var t = node.nodeType;
    return t == 3 || t == 4 || t == 8 ; // Text, CDataSection or Comment
}

function splitRangeBoundaries(range) {
    var sc = range.startContainer, so = range.startOffset, ec = range.endContainer, eo = range.endOffset;
    var startEndSame = (sc === ec);

    // Split the end boundary if necessary
    if (isCharacterDataNode(ec) && eo > 0 && eo < ec.length) {
        splitDataNode(ec, eo);
    }

    // Split the start boundary if necessary
    if (isCharacterDataNode(sc) && so > 0 && so < sc.length) {
        sc = splitDataNode(sc, so);
        if (startEndSame) {
            eo -= so;
            ec = sc;
        } else if (ec == sc.parentNode && eo >= getNodeIndex(sc)) {
            ++eo;
        }
        so = 0;
    }
    range.setStart(sc, so);
    range.setEnd(ec, eo);
}

function getTextNodesInRange(range) {
    var textNodes = [];
    var nodes = getNodesInRange(range);
    for (var i = 0, node, el; node = nodes[i++]; ) {
        if (node.nodeType == 3) {
            textNodes.push(node);
        }
    }
    return textNodes;
}

function surroundRangeContents(range, templateElement) {
    splitRangeBoundaries(range);
    var textNodes = getTextNodesInRange(range);
    if (textNodes.length == 0) {
        return;
    }
    for (var i = 0, node, el; node = textNodes[i++]; ) {
        if (node.nodeType == 3) {
            el = templateElement.cloneNode(false);
            node.parentNode.insertBefore(el, node);
            el.appendChild(node);
        }
    }
    range.setStart(textNodes[0], 0);
    var lastTextNode = textNodes[textNodes.length - 1];
    range.setEnd(lastTextNode, lastTextNode.length);
}

document.onmouseup = function() {
    if (window.getSelection) {
        var templateElement = document.createElement("span");
        templateElement.className = "highlight";
        var sel = window.getSelection();
        var ranges = [];
        var range;
        for (var i = 0, len = sel.rangeCount; i < len; ++i) {
            ranges.push( sel.getRangeAt(i) );
        }
        sel.removeAllRanges();

        // Surround ranges in reverse document order to prevent surrounding subsequent ranges messing with already-surrounded ones
        i = ranges.length;
        while (i--) {
            range = ranges[i];
            surroundRangeContents(range, templateElement);
            sel.addRange(range);
        }
    }
};
.highlight {
  font-weight: bold;
  color: red;
}
Select some of this text and it will be highlighted:

<ul>
    <li>the <b>only entry</b> of the list</li>
</ul>
<p>Some text here in paragraph</p>

<ul>
    <li>the <b>only entry</b> of the list</li>
</ul>
<p>Some text here in paragraph</p>

<ul>
    <li>the <b>only entry</b> of the list</li>
</ul>
<p>Some text here in paragraph</p>

发布评论

评论列表(0)

  1. 暂无评论