javascript contentEditable - wrap cross-tag selections

杀马特。学长 韩版系。学妹 提交于 2019-12-02 06:16:26
Tim Down

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 commonAncestor = range.commonAncestorContainer;
    var nodes = [];
    var node;

    // Walk parent nodes from start to common ancestor
    for (node = start.parentNode; node; node = node.parentNode) {
        nodes.push(node);
        if (node == commonAncestor) {
            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>
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!