27

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.

drs
  • 5,679
  • 4
  • 42
  • 67
alt
  • 13,357
  • 19
  • 80
  • 120
  • in that case you will have to repace the text node with html content – Arun P Johny May 21 '13 at 04:57
  • @ArunPJohny How do I do that? – alt May 21 '13 at 04: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. – RobG May 21 '13 at 05:01
  • 1
    @ArunPJohny Text nodes do not have an innerHTMl property. – alt May 21 '13 at 05: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 – Arun P Johny May 21 '13 at 05:10
  • `<span>lol</span>` should work I suppose. (Insert in HTML instead, then it could be easily get into text node, I think so). – Mr_Green May 21 '13 at 05:12
  • Click [here](http://stackoverflow.com/questions/7698673/add-html-to-text-node-extracted-via-node-nodevalue), this might help. – Suraj Jadhav May 21 '13 at 05:16
  • maybe this SO can help you to solve it, if you can use jquery http://stackoverflow.com/questions/7698673/add-html-to-text-node-extracted-via-node-nodevalue – fmodos May 21 '13 at 05:20

5 Answers5

20

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.

Jeff
  • 13,943
  • 11
  • 55
  • 103
  • 1
    I added a fix `regex.lastIndex = 0` to reset the regex when reusing it. See http://stackoverflow.com/questions/1520800/why-regexp-with-global-flag-in-javascript-give-wrong-results – Jeff May 12 '16 at 20:15
  • When you get a chance could you please clarify where the text and replacement is happening adding (put text here) put (replacement text here) – Fred Mcgiff May 03 '18 at 16:34
  • `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. – d'Artagnan Evergreen Barbosa Dec 30 '18 at 20:11
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.
alex
  • 479,566
  • 201
  • 878
  • 984
Andreas Josas
  • 242
  • 2
  • 7
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);
BrianK
  • 2,357
  • 3
  • 32
  • 41
  • Thanks for that, it was the best answer for me – romuleald Jan 27 '17 at 09:53
  • 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. – Fred Mcgiff May 03 '18 at 16:33
  • When you get a chance could you please clarify where the text and replacement is happening adding (put text here) put (replacement text here) – Fred Mcgiff May 03 '18 at 16:33
4

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);
liaguridio
  • 461
  • 5
  • 13
3

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.

Sam Dutton
  • 14,775
  • 6
  • 54
  • 64
anomaaly
  • 833
  • 4
  • 15
  • 9
    Be warned replacing the innerHTML of the containing node is destructive to child nodes that may have had event listeners added via scripts. – ccjuju Aug 11 '14 at 21:52
  • 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. – Jeff May 12 '16 at 18:49
  • If parent.Node = body, there are probably many siblings, so how would you know which child to replace? – Elliott B Jun 11 '18 at 18:32
  • 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. – lorg Sep 30 '19 at 19:36