Это не просто и не элегантно, но работает как положено, без дополнительной разметки и является лучшим, что я могу придумать.
По сути, вы должны пересечь дерево dom, пересекающееся с диапазоном выбора, и собрать поддиапазоны, состоящие только из текстового узла во время обхода.
Просмотрите Документацию по диапазону для получения информации о startContainer
и endContainer
.
Проще говоря, они одинаковы при выборе в одном текстовом узле, и в противном случае они дают вам начальную и конечную точку вашего обхода.
Как только вы соберете эти диапазоны, вы можете обернуть их в тег по своему вкусу.
Это работает довольно хорошо, но, к сожалению, я не смог сохранить первоначальный выбор после полужирного шрифта (пробовал все с selection.setRange(..)
безуспешно):
document.getElementById("bold").onclick = function() {
var selection = document.getSelection(),
range = selection.getRangeAt(0).cloneRange();
// start and end are always text nodes
var start = range.startContainer;
var end = range.endContainer;
var ranges = [];
// if start === end then it's fine we have selected a portion of a text node
while (start !== end) {
var startText = start.nodeValue;
var currentRange = range.cloneRange();
// pin the range at the end of this text node
currentRange.setEnd(start, startText.length);
// keep the range for later
ranges.push(currentRange);
var sibling = start;
do {
if (sibling.hasChildNodes()) {
// if it has children then it's not a text node, go deeper
sibling = sibling.firstChild;
} else if (sibling.nextSibling) {
// it has a sibling, go for it
sibling = sibling.nextSibling;
} else {
// we're into a corner, we have to go up one level and go for next sibling
while (!sibling.nextSibling && sibling.parentNode) {
sibling = sibling.parentNode;
}
if (sibling) {
sibling = sibling.nextSibling;
}
}
} while (sibling !== null && sibling.nodeValue === null);
if (!sibling) {
// out of nodes!
break;
}
// move range start to the identified next text node (sibling)
range.setStart(sibling, 0);
start = range.startContainer;
}
// surround all collected range by the b tag
for (var i = 0; i < ranges.length; i++) {
var currentRange = ranges[i];
currentRange.surroundContents(document.createElement("b"));
}
// surround the remaining range by a b tag
range.surroundContents(document.createElement("b"));
// unselect everything because I can't presere the original selection
selection.removeAllRanges();
}
<div contenteditable="true" id="div">This is the editor. If you embolden only **this**, it will work. If you try <font color="red">to embolden **this <i>and this**</i>, it will work <font color="green">because</font> we are traversing the</font> nodes<table rules="all">
<tr><td>it</td><td>will<td>even</td><td>work</td></tr>
<tr><td>in</td><td>more</td><td>complicated</td><td><i>markup</i></td></tr>
</table></div>
<button id="bold">Bold</button>