Reputation: 13907
I've got a little text node:
var node
And I want to wrap a span around every occurrence of "lol".
node.nodeValue = node.nodeValue.replace(/lol/, "<span>lol</span>")
It it prints out "<span>lol<span>"
when I want "lol"
as a span element.
Upvotes: 28
Views: 26340
Reputation: 571
An up to date answer for those that are finding this question now is the following :
function textNodeInnerHTML(textNode,innerHTML) {
var div = document.createElement('div');
textNode.parentNode.insertBefore(div,textNode);
div.insertAdjacentHTML('afterend',innerHTML);
div.remove();
textNode.remove();
}
The idea is to insert a newly created html element (lets say var div = document.createElement('div');
) before the textNode
using :
textNode.parentNode.insertBefore(div,textNode);
and then use :
div.insertAdjacentHTML(
'afterend',
textNode.data.replace(/lol/g,`<span style="color : red">lol</span>`)
)
then remove textNode
and div
using :
textNode.remove();
div.remove();
The insertAdjacentHTML
does not destroy event listeners like innerHTML
does .
If you want to find all text nodes that are descendants of elm
then use :
[...elm.querySelectorAll('*')]
.map(l => [...l.childNodes])
.flat()
.filter(l => l.nodeType === 3);
Upvotes: 6
Reputation: 875
The answer presented by Andreas Josas is quite good. However the code had several bugs when the search term appeared several times in the same text node. Here is the solution with those bugs fixed and additionally the insert is factored up into matchText for easier use and understanding. Now only the new tag is constructed in the callback and passed back to matchText by a return.
Updated matchText function with bug fixes:
var matchText = function(node, regex, callback, excludeElements) {
excludeElements || (excludeElements = ['script', 'style', 'iframe', 'canvas']);
var child = node.firstChild;
while (child) {
switch (child.nodeType) {
case 1:
if (excludeElements.indexOf(child.tagName.toLowerCase()) > -1)
break;
matchText(child, regex, callback, excludeElements);
break;
case 3:
var bk = 0;
child.data.replace(regex, function(all) {
var args = [].slice.call(arguments),
offset = args[args.length - 2],
newTextNode = child.splitText(offset+bk), tag;
bk -= child.data.length + all.length;
newTextNode.data = newTextNode.data.substr(all.length);
tag = callback.apply(window, [child].concat(args));
child.parentNode.insertBefore(tag, newTextNode);
child = newTextNode;
});
regex.lastIndex = 0;
break;
}
child = child.nextSibling;
}
return node;
};
Usage:
matchText(document.getElementsByTagName("article")[0], new RegExp("\\b" + searchTerm + "\\b", "g"), function(node, match, offset) {
var span = document.createElement("span");
span.className = "search-term";
span.textContent = match;
return span;
});
If you desire to insert anchor (link) tags instead of span tags, change the create element to be "a" instead of "span", add a line to add the href attribute to the tag, and add 'a' to the excludeElements list so that links will not be created inside links.
Upvotes: 21
Reputation: 2705
Not saying this is a better answer, but I'm posting what I did for completeness. In my case I have already looked up or determined the offsets of the text that I needed to highlight in a particular #text node. This also clarifies the steps.
//node is a #text node, startIndex is the beginning location of the text to highlight, and endIndex is the index of the character just after the text to highlight
var parentNode = node.parentNode;
// break the node text into 3 parts: part1 - before the selected text, part2- the text to highlight, and part3 - the text after the highlight
var s = node.nodeValue;
// get the text before the highlight
var part1 = s.substring(0, startIndex);
// get the text that will be highlighted
var part2 = s.substring(startIndex, endIndex);
// get the part after the highlight
var part3 = s.substring(endIndex);
// replace the text node with the new nodes
var textNode = document.createTextNode(part1);
parentNode.replaceChild(textNode, node);
// create a span node and add it to the parent immediately after the first text node
var spanNode = document.createElement("span");
spanNode.className = "HighlightedText";
parentNode.insertBefore(spanNode, textNode.nextSibling);
// create a text node for the highlighted text and add it to the span node
textNode = document.createTextNode(part2);
spanNode.appendChild(textNode);
// create a text node for the text after the highlight and add it after the span node
textNode = document.createTextNode(part3);
parentNode.insertBefore(textNode, spanNode.nextSibling);
Upvotes: 5
Reputation: 242
The following article gives you the code to replace text with HTML elements:
http://blog.alexanderdickson.com/javascript-replacing-text
From the article:
var matchText = function(node, regex, callback, excludeElements) {
excludeElements || (excludeElements = ['script', 'style', 'iframe', 'canvas']);
var child = node.firstChild;
do {
switch (child.nodeType) {
case 1:
if (excludeElements.indexOf(child.tagName.toLowerCase()) > -1) {
continue;
}
matchText(child, regex, callback, excludeElements);
break;
case 3:
child.data.replace(regex, function(all) {
var args = [].slice.call(arguments),
offset = args[args.length - 2],
newTextNode = child.splitText(offset);
newTextNode.data = newTextNode.data.substr(all.length);
callback.apply(window, [child].concat(args));
child = newTextNode;
});
break;
}
} while (child = child.nextSibling);
return node;
}
Usage:
matchText(document.getElementsByTagName("article")[0], new RegExp("\\b" + searchTerm + "\\b", "g"), function(node, match, offset) {
var span = document.createElement("span");
span.className = "search-term";
span.textContent = match;
node.parentNode.insertBefore(span, node.nextSibling);
});
And the explanation:
Essentially, the right way to do it is…
- Iterate over all text nodes.
- Find the substring in text nodes.
- Split it at the offset.
- Insert a span element in between the split.
Upvotes: 17
Reputation: 841
You may need node
to be the parent node, that way you can just use innerHTML:
node.innerHTML=node.childNodes[0].nodeValue.replace(/lol/, "<span>lol</span>");
Here node.childNodes[0]
refers to the actual text node, and node
is its containing element.
Upvotes: 4