28

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.

8
  • in that case you will have to repace the text node with html content Commented May 21, 2013 at 4:57
  • @ArunPJohny How do I do that? Commented May 21, 2013 at 4:59
  • @JacksonGariety—you need to replace the text node with new DOM span element and text nodes, or modify its parent's innerHTML property. Have a go, post what you try. Commented May 21, 2013 at 5:01
  • 1
    @ArunPJohny Text nodes do not have an innerHTMl property. Commented May 21, 2013 at 5:08
  • @JacksonGariety you cann't just set it like that, you may have to right some code to replace the text node. Can you share the html for the text node Commented May 21, 2013 at 5:10

5 Answers 5

21

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.

Sign up to request clarification or add additional context in comments.

3 Comments

I added a fix regex.lastIndex = 0 to reset the regex when reusing it. See stackoverflow.com/questions/1520800/…
When you get a chance could you please clarify where the text and replacement is happening adding (put text here) put (replacement text here)
searchTerm is the text you are searching for. The callback you supply receives the dom node, the match found, and the offset of what is found. You then do whatever you want with that knowledge in your callback, like replace it, color the node, split the text out and put a tag around it, or whatever you desire now that you know that exact spot in the dom where your searchTerm was found.
17

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…

  1. Iterate over all text nodes.
  2. Find the substring in text nodes.
  3. Split it at the offset.
  4. Insert a span element in between the split.

2 Comments

This is issues for multiple matches within a text node: jsfiddle.net/vw0eok5t
Should add if (child===null) return; after var child = node.firstChild;.
6

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);

Comments

5

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);

2 Comments

How does one use this for numbers? Could you give example of finding and replacing a numbers . For example incorrect zip code replaced with zip code. Number but as you can see this is for text and i still trying to figure out replace numbers.
When you get a chance could you please clarify where the text and replacement is happening adding (put text here) put (replacement text here)
4

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.

4 Comments

Be warned replacing the innerHTML of the containing node is destructive to child nodes that may have had event listeners added via scripts.
This doesnt work for me (Chrome 50); there is no visual change. I can see in the console that node.innerHTML has changed, but there is no change to the UI. Also, node.parentNode.innerHTML does not contain the new change.
If parent.Node = body, there are probably many siblings, so how would you know which child to replace?
For future readers - there is another important problem - this answer is turning text contents into html. If the text contents happen to have html tags in them, then they will not be escaped, and at best this will generate unexpected results, at worst this could be a security risk.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.