I have a contenteditable div
, and I want to split a node around a selection. Using execCommand()
, I can toggle "bold" on or off for a selection, so if I have:
<b>ABCDEFGHI</b>
and select DEF
, toggling "bold" gives me
<b>ABC</b>DEF<b>GHI</b>
where the <b>
node has been split into two <b>
nodes with a text node in between.
I want to be able to do the same with other elements not supported by execCommand()
, for example <bdi>
. In other words, if I start with
<bdi>ABCDEFGHI</bdi>
and select DEF
, I want to end up with
<bdi>ABC</bdi>DEF<bdi>GHI</bdi>
I can test if the selection is contained in a surrounding <bdi>
tag using range.commonAncestorContainer()
and if not wrap the range in a <bdi>
tag. However, what I want is the opposite: if there is an enclosing <bdi>
node, I want to split it into (a) a well-formed <bdi>
node before the selection, (b) a well-formed selection with no enclosing <bdi>
, and (c) another well-formed <bdi>
node after the selection, and then reassemble them. How can I do this?
EDIT: it seems that everyone believes I am trying to wrap a selection, but I'm not. Sergey's response below shows how to wrap some plain text, but I want something else.
Trying for a minimal reproducible example, consider the following:
<html>
<head></head>
<body>
<b>This text is <i>marked as
bold
with</i> some italic text too.</b>
</body>
</html>
Now what I want is to UNMARK the text "bold" so that the final result is:
<html>
<head></head>
<body>
<b>This text is <i>marked as</i></b>
<i>bold</i>
<b><i>with</i> some italic text too.</b>
</body>
</html>
Note that the text includes <i>
...</i>
, which must also be split. This is trivially easy with execCommand()
, but I can't figure out how to do it without execCommand()
(and hence do it for tags like <bdi>
as well). I'm looking for a vanilla JS solutsion, not jQuery or Rangy please.
I have finally come up with a solution: first I wrap the selection in a <span>
. Then I work up the tree from the selection range's commonAncestorContainer
to the node I want to replace (<bdi>
in this case, but this will work with any other element as well). I then create two <bdi>
elements (before
and after
) and copy the child nodes into before
until I get to the <span>
node I created, and copy the rest to after
.
Finally I use insertBefore()
on the <bdi>
's parent node to insert before
in front of <bdi>
, then the child nodes of the <span>
element, followed by after
. I then remove the original <bdi>
.
Result: the original <bdi>
element has been replaced by a well-formed <bdi>
node, followed by some unwrapped nodes, followed by another well-formed <bdi>
node.
Oh, and if there is no enclosing <bdi>
node, I just create one and use range.surroundContents()
to wrap the selection, so that <bdi>
is toggled on and off like execCommand()
does for certain other tags.
Here is the code:
const copy = range.cloneContents();
//
// Test if selection is inside a BDI
//
let bdi = null;
for (let c = range.commonAncestorContainer; c != null; c = c.parentElement) {
if (c.nodeType == 1 && c.nodeName == "BDI") {
bdi = c;
break;
}
}
if (!!bdi) {
//
// Wrap the range in a <span>
//
const span = document.createElement("span");
span.appendChild(copy);
range.deleteContents();
range.insertNode(span);
//
// Now split the enclosing BDI node before and after the <span> node
//
const before = document.createElement("bdi");
const after = document.createElement("bdi");
let found = false;
for (let c = bdi.firstChild; c != null; c = bdi.firstChild) {
if (found) {
after.appendChild(c);
}
else if (c == span) {
found = true;
bdi.removeChild(bdi.firstChild);
}
else {
before.appendChild(c);
}
}
//
// Now insert "before", the <span> node contents and "after"
// in front of the BDI node, and remove the original BDI node
//
const p = bdi.parentElement;
p.insertBefore(before,bdi);
for (var c = span.firstChild; c != null; c = span.firstChild) {
p.insertBefore(c,bdi);
}
p.insertBefore(after,bdi);
p.removeChild(bdi);
}
else {
//
// No enclosing BDI, so wrap the selection
//
const bdi = document.createElement("bdi");
range.deleteContents();
range.insertNode(copy);
range.surroundContents(bdi);
}