Reputation: 754
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!
Upvotes: 3
Views: 2027
Reputation: 324627
The basic approach if you're only interested in wrapping the text parts is:
<span>
elementThis 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>
Upvotes: 12