By Christian Oudard


2008-11-18 13:45:09 8 Comments

I would like to get all descendant text nodes of an element, as a jQuery collection. What is the best way to do that?

11 comments

@Salman A 2014-01-19 10:29:22

jQuery.contents() can be used with jQuery.filter to find all child text nodes. With a little twist, you can find grandchildren text nodes as well. No recursion required:

$(function() {
  var $textNodes = $("#test, #test *").contents().filter(function() {
    return this.nodeType === Node.TEXT_NODE;
  });
  /*
   * for testing
   */
  $textNodes.each(function() {
    console.log(this);
  });
});
div { margin-left: 1em; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>

<div id="test">
  child text 1<br>
  child text 2
  <div>
    grandchild text 1
    <div>grand-grandchild text 1</div>
    grandchild text 2
  </div>
  child text 3<br>
  child text 4
</div>

jsFiddle

@Tim Down 2010-12-09 15:13:26

jQuery doesn't have a convenient function for this. You need to combine contents(), which will give just child nodes but includes text nodes, with find(), which gives all descendant elements but no text nodes. Here's what I've come up with:

var getTextNodesIn = function(el) {
    return $(el).find(":not(iframe)").addBack().contents().filter(function() {
        return this.nodeType == 3;
    });
};

getTextNodesIn(el);

Note: If you're using jQuery 1.7 or earlier, the code above will not work. To fix this, replace addBack() with andSelf(). andSelf() is deprecated in favour of addBack() from 1.8 onwards.

This is somewhat inefficient compared to pure DOM methods and has to include an ugly workaround for jQuery's overloading of its contents() function (thanks to @rabidsnail in the comments for pointing that out), so here is non-jQuery solution using a simple recursive function. The includeWhitespaceNodes parameter controls whether or not whitespace text nodes are included in the output (in jQuery they are automatically filtered out).

Update: Fixed bug when includeWhitespaceNodes is falsy.

function getTextNodesIn(node, includeWhitespaceNodes) {
    var textNodes = [], nonWhitespaceMatcher = /\S/;

    function getTextNodes(node) {
        if (node.nodeType == 3) {
            if (includeWhitespaceNodes || nonWhitespaceMatcher.test(node.nodeValue)) {
                textNodes.push(node);
            }
        } else {
            for (var i = 0, len = node.childNodes.length; i < len; ++i) {
                getTextNodes(node.childNodes[i]);
            }
        }
    }

    getTextNodes(node);
    return textNodes;
}

getTextNodesIn(el);

@crosenblum 2011-02-10 15:56:23

Can the element passed in, be the name of a div?

@Tim Down 2011-02-10 16:43:42

@crosenblum: You could call document.getElementById() first, if that's what you mean: var div = document.getElementById("foo"); var textNodes = getTextNodesIn(div);

@bobpoekert 2012-02-03 00:29:33

Because of a bug in jQuery if you have any iframes in el you'll need to use .find(':not(iframe)') instead of .find('*') .

@Tim Down 2012-02-03 09:42:57

@rabidsnail: What's the bug?

@Robin Maben 2012-02-06 11:52:17

@rabidsnail: I think, the use of .contents() anyways implies it will search through the iframe as well. I don't see how it could be a bug.

@bobpoekert 2012-02-13 21:31:11

bugs.jquery.com/ticket/11275 Whether this is actually a bug seems to be up for debate, but bug or not if you call find('*').contents() on a node that contains an iframe which hasn't been added to the dom you'll get an exception at an undefined point.

@Tim Down 2012-02-13 21:57:26

@rabidsnail: OK, I think that's at least an annoyance (if not a bug) in jQuery and a point in favour of the plain DOM version. I'll edit my answer. Thanks.

@Mottie 2013-02-14 14:03:57

andSelf() was deprecated in jQuery 1.8, you can use addBack() instead.

@ErikE 2013-11-18 23:29:41

You could consider nonWhitespace = /\S/ and if (includeWhitespaceNodes || nonWhitespace.test(node.nodeValue)) { which at least boasts greater simplicity (though it would respond differently to empty text nodes, if those are possible). I also think there could be improvement in the regex variable name... something like whitespaceMatcher or something to indicate what the variable is.

@Tim Down 2013-11-18 23:42:10

@ErikE: I like descriptive variable names. I have a feeling I picked whitespace to avoid the code having horizontal scrollbars on my browser.

@Tim Down 2013-11-18 23:55:05

@ErikE: I agree with you on both counts and have edited my answer. Empty text nodes are indeed possible but will be treated the same by both !/^\s*$/.test() and /\S/.test() so there's no problem there.

@ErikE 2013-11-19 00:03:06

oh, right, it was * not + so empty nodes were matched before. Glad you liked my suggestions!

@Ben Siver 2014-03-28 14:58:27

Great suggestion. I would recommend using Node.TextNode in place of 3 for better readability.

@Tim Down 2014-03-28 16:31:54

@BenS: I would but Node.TEXT_NODE isn't supported in IE <= 8.

@Brian Geihsler 2014-06-11 18:55:05

This code has a bug in it. Right now when you pass false for including whitespace, it ONLY modifies whitespace nodes instead of excluding them. The line if (includeWhitespaceNodes || !nonWhitespaceMatcher.test(node.nodeValue)) should instead read: if (includeWhitespaceNodes || nonWhitespaceMatcher.test(node.nodeValue)).

@Tim Down 2014-06-11 22:35:09

@BrianGeihsler: You're right, thanks. I simplified the regular expression last November but failed to negate the condition. Wish I'd tested it now.

@Alex W 2014-10-13 18:03:57

I was getting a lot of empty text nodes with the accepted filter function. If you're only interested in selecting text nodes that contain non-whitespace, try adding a nodeValue conditional to your filter function, like a simple $.trim(this.nodevalue) !== '':

$('element')
    .contents()
    .filter(function(){
        return this.nodeType === 3 && $.trim(this.nodeValue) !== '';
    });

http://jsfiddle.net/ptp6m97v/

Or to avoid strange situations where the content looks like whitespace, but is not (e.g. the soft hyphen &shy; character, newlines \n, tabs, etc.), you can try using a Regular Expression. For example, \S will match any non-whitespace characters:

$('element')
        .contents()
        .filter(function(){
            return this.nodeType === 3 && /\S/.test(this.nodeValue);
        });

@iConnor 2014-02-16 05:16:35

For some reason contents() didn't work for me, so if it didn't work for you, here's a solution I made, I created jQuery.fn.descendants with the option to include text nodes or not

Usage


Get all descendants including text nodes and element nodes

jQuery('body').descendants('all');

Get all descendants returning only text nodes

jQuery('body').descendants(true);

Get all descendants returning only element nodes

jQuery('body').descendants();

Coffeescript Original:

jQuery.fn.descendants = ( textNodes ) ->

    # if textNodes is 'all' then textNodes and elementNodes are allowed
    # if textNodes if true then only textNodes will be returned
    # if textNodes is not provided as an argument then only element nodes
    # will be returned

    allowedTypes = if textNodes is 'all' then [1,3] else if textNodes then [3] else [1]

    # nodes we find
    nodes = []


    dig = (node) ->

        # loop through children
        for child in node.childNodes

            # push child to collection if has allowed type
            nodes.push(child) if child.nodeType in allowedTypes

            # dig through child if has children
            dig child if child.childNodes.length


    # loop and dig through nodes in the current
    # jQuery object
    dig node for node in this


    # wrap with jQuery
    return jQuery(nodes)

Drop In Javascript Version

var __indexOf=[].indexOf||function(e){for(var t=0,n=this.length;t<n;t++){if(t in this&&this[t]===e)return t}return-1}; /* indexOf polyfill ends here*/ jQuery.fn.descendants=function(e){var t,n,r,i,s,o;t=e==="all"?[1,3]:e?[3]:[1];i=[];n=function(e){var r,s,o,u,a,f;u=e.childNodes;f=[];for(s=0,o=u.length;s<o;s++){r=u[s];if(a=r.nodeType,__indexOf.call(t,a)>=0){i.push(r)}if(r.childNodes.length){f.push(n(r))}else{f.push(void 0)}}return f};for(s=0,o=this.length;s<o;s++){r=this[s];n(r)}return jQuery(i)}

Unminified Javascript version: http://pastebin.com/cX3jMfuD

This is cross browser, a small Array.indexOf polyfill is included in the code.

@Guillermo 2011-07-30 17:47:35

I had the same problem and solved it with:

Code:

$.fn.nextNode = function(){
  var contents = $(this).parent().contents();
  return contents.get(contents.index(this)+1);
}

Usage:

$('#my_id').nextNode();

Is like next() but also returns the text nodes.

@Michel de Ruiter 2012-01-16 11:24:01

What about .nextSibling?

@Guillermo 2012-02-14 10:15:18

.nextSibling is from Dom specification: developer.mozilla.org/en/Document_Object_Model_(DOM)/…

@Michel de Ruiter 2012-02-14 19:26:20

So? Is that forbidden?

@Mr_Green 2013-06-19 11:24:08

Can also be done like this:

var textContents = $(document.getElementById("ElementId").childNodes).filter(function(){
        return this.nodeType == 3;
});

The above code filters the textNodes from direct children child nodes of a given element.

@Tim Down 2013-10-17 21:15:54

... but not all the descendant child nodes (e.g. a text node that is the child of an element that is a child of the original element).

@davenpcj 2013-08-23 14:41:21

For me, plain old .contents() appeared to work to return the text nodes, just have to be careful with your selectors so that you know they will be text nodes.

For example, this wrapped all the text content of the TDs in my table with pre tags and had no problems.

jQuery("#resultTable td").content().wrap("<pre/>")

@He Nrik 2011-10-19 16:07:40

$('body').find('*').contents().filter(function () { return this.nodeType === 3; });

@Christian Oudard 2008-11-18 13:47:45

Jauco posted a good solution in a comment, so I'm copying it here:

$(elem)
  .contents()
  .filter(function() {
    return this.nodeType === 3; //Node.TEXT_NODE
  });

@Jauco 2009-07-11 13:53:14

actually $(elem) .contents() .filter(function() { return this.nodeType == Node.TEXT_NODE; }); is enough

@Christian Oudard 2009-12-29 20:00:54

IE7 doesn't define the Node global, so you have to use this.nodeType == 3, unfortunately: stackoverflow.com/questions/1423599/node-textnode-and-ie7

@Tim Down 2010-10-15 14:12:28

Does this not only return the text nodes that are the direct children of the element rather than descendants of the element as the OP requested?

@Christian Oudard 2010-11-29 19:42:50

I think you're right. Care to submit an improvement?

@Tim Down 2012-10-10 22:35:53

I've just noticed that your first answer from 2008 was almost exactly what I independently came up with much later. Why did you edit it?

@Shahar 2013-09-29 08:14:35

add .text() at the end if you want it so be a string. Otherwise it's still an object. Trying to show it in the document will end up displaying [Object object].

@mpen 2014-06-10 21:28:25

@ChristianOudard That would be really easy to polyfill, no? Would make your code a bit more legible.

@minhajul 2015-10-16 16:08:52

this will not work when the text node is deep nested inside other elements, because the contents() method only returns the immediate children nodes, api.jquery.com/contents

@minhajul 2015-10-16 16:14:49

@Jauco, nope, not enough! as .contents() returns only the immediate children nodes

@Rahen Rangan 2011-06-22 18:36:14

if you want to strip all tags, then try this

function:

String.prototype.stripTags=function(){
var rtag=/<.*?[^>]>/g;
return this.replace(rtag,'');
}

usage:

var newText=$('selector').html().stripTags();

@colllin 2011-04-20 14:52:19

If you can make the assumption that all children are either Element Nodes or Text Nodes, then this is one solution.

To get all child text nodes as a jquery collection:

$('selector').clone().children().remove().end().contents();

To get a copy of the original element with non-text children removed:

$('selector').clone().children().remove().end();

@colllin 2011-04-20 14:58:47

Just noticed Tim Down's comment on another answer. This solution only gets the direct children, not all descendents.

Related Questions

Sponsored Content

33 Answered Questions

[SOLVED] How can I upload files asynchronously?

27 Answered Questions

[SOLVED] What does "use strict" do in JavaScript, and what is the reasoning behind it?

41 Answered Questions

[SOLVED] How do I remove a property from a JavaScript object?

88 Answered Questions

[SOLVED] How do I remove a particular element from an array in JavaScript?

  • 2011-04-23 22:17:18
  • Walker
  • 6152623 View
  • 7678 Score
  • 88 Answer
  • Tags:   javascript arrays

31 Answered Questions

42 Answered Questions

[SOLVED] Is there an "exists" function for jQuery?

  • 2008-08-27 19:49:41
  • Jake McGraw
  • 729635 View
  • 2671 Score
  • 42 Answer
  • Tags:   javascript jquery

3 Answered Questions

55 Answered Questions

[SOLVED] How do I check if an element is hidden in jQuery?

35 Answered Questions

[SOLVED] Add table row in jQuery

34 Answered Questions

Sponsored Content