0

Don't ask why I need this. What I need is some simple procedure, if there is one, to replace every instance of a word with another word. I'm not that experienced with JQuery, but I know it would be something equivalent to

$(document).ready(function(){
    var allElements = $('*');
    for each (var thisElement in allElements)
    {
        var newText = thisElement.text().replace("foo", "bar"); 
        thisElement.text(newText);     
    }
});

Would that work? Is there a better way? Deliver me a 1-liner if possible.

  • 1
    Can give you a one liner that replaces the whole body html...but could break so many things. Don't worry about code length vs getting the job done properly – charlietfl Mar 05 '15 at 23:50

1 Answers1

4

What you have tried would not work because it would strip all HTML from parent elements (thus removing all child elements). If using .text(), you have to only do replace operations only on textNodes to avoid accidentally killing childNodes which will kill the structure of your document.

In your code, the very first parent element you do your replace on will get the .text() from it (which returns only text, not child elements) and then set .text() back on it. This ends up removing all child elements from the document which isn't what you want to do.

The only way I know of to do this is to iterate through all the individual text nodes in the document and do a replace on each one.

Since you asked for a jQuery method of doing this, here's one (that I don't think it is particularly efficient), but it appears to work:

$(document.body).find("*").contents().each(function() {
    if (this.nodeType === 3) {
        this.nodeValue = this.nodeValue.replace(/foo/g, "bar");
    }
});

And, if you really want it on one line (which wrecks readability if you ask me), you could do this:

$(document.body).find("*").contents().filter(function() {return this.nodeType === 3}).each(function() {this.nodeValue = this.nodeValue.replace(/foo/g, "bar")});

This finds all elements in the body, then gets all the child nodes of each one of those elements, then iterates every one of those nodes looking only for text nodes to do a replace on.

Working demo: http://jsfiddle.net/jfriend00/qw8va10y/


A method that is likely much more efficient can be done in plain Javascript.

This code uses a tree walking function I've previously developed which lets you iterate through all the nodes in an parent node and knows to skip certain types of tags that it shouldn't iterate. It takes a callback which can then examine each node and decide what to do:

var treeWalkFast = (function() {
    // create closure for constants
    var skipTags = {"SCRIPT": true, "IFRAME": true, "OBJECT": true, 
        "EMBED": true, "STYLE": true, "LINK": true, "META": true};
    return function(parent, fn, allNodes) {
        var node = parent.firstChild, nextNode;
        while (node && node != parent) {
            if (allNodes || node.nodeType === 1) {
                if (fn(node) === false) {
                    return(false);
                }
            }
            // if it's an element &&
            //    has children &&
            //    has a tagname && is not in the skipTags list
            //  then, we can enumerate children
            if (node.nodeType === 1 && node.firstChild && !(node.tagName && skipTags[node.tagName])) {
                node = node.firstChild;
            } else  if (node.nextSibling) {
                node = node.nextSibling;
            } else {
                // no child and no nextsibling
                // find parent that has a nextSibling
                while ((node = node.parentNode) != parent) {
                    if (node.nextSibling) {
                        node = node.nextSibling;
                        break;
                    }
                }
            }
        }
    }
})();

treeWalkFast(document.body, function(node) {
    // process only text nodes
    if (node.nodeType == 3) {
        node.nodeValue = node.nodeValue.replace(/foo/g, "bar");
    }
}, true);

Working demo: http://jsfiddle.net/jfriend00/Lvbr4a00/

The only cases that I know of where this would not work is if the text had HTML tags in the middle of it (and thus it is split across more than one text node). That is a fairly complicated problem to solve because you have to figure out how to handle the HTML formatting in the middle of the thing you're replacing the text of.


And, here's a jQuery plug-in that fetches textNodes (using the treeWalkFast() function):

jQuery.fn.textNodes = function() {
    var nodes = [];
    this.each(function() {
        treeWalkFast(this, function(node) {
            if (node.nodeType === 3) {
                nodes.push(node);
            }
        }, true);
    });
    return this.pushStack(nodes, "textNodes");
}

And, a jQuery plug-in that uses that to do a find/replace:

// find text may be regex or string
// if using regex, use the g flag so it will replace all occurrences
jQuery.fn.findReplace = function(find, replace) {
    var fn;
    if (typeof find === "string") {
        fn = function(node) {
            var lastVal;
            // loop calling replace until the string doesn't change any more
            do {
                lastVal = node.nodeValue;
                node.nodeValue = lastVal.replace(find, replace);
            } while (node.nodeValue !== lastVal);
        }
    } else if (find instanceof RegExp) {
        fn = function(node) {
            node.nodeValue = node.nodeValue.replace(find, replace);
        }
    } else {
        throw new Error("find argument must be string or RegExp");
    }
    return this.each(function() {
        $(this).textNodes().each(function() {
            fn(this);
        });
    });
}

So, once you have these plug-ins and the treeWalkFast() supporting code installed, you can then just do this:

$(document.body).findReplace("foo", "bar");

Working demo: http://jsfiddle.net/jfriend00/7g1Lfvcm/

jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • Added a jQuery version that is shorter, but much less efficient than the plain Javascript version. – jfriend00 Mar 05 '15 at 23:56
  • @charlietfl - Good catch. Fixed by making the `.replace()` use a regex with the `g` flag. Could also loop doing multiple replace operations if the OP doesn't want to use a regex. – jfriend00 Mar 06 '15 at 00:05
  • Added one liner version (which really just crams a bunch of chained operations onto one line) for grins (not my favorite way to express code). – jfriend00 Mar 06 '15 at 01:06
  • Added jQuery plug-in that makes it truly a one-liner (once you have the plugins and supporting code installed). – jfriend00 Mar 06 '15 at 03:43