ChatGPT解决这个技术问题 Extra ChatGPT

Using .text() to retrieve only text not nested in child tags

If I have html like this:

<li id="listItem">
    This is some text
    <span id="firstSpan">First span text</span>
    <span id="secondSpan">Second span text</span>
</li>

I'm trying to use .text() to retrieve just the string "This is some text", but if I were to say $('#list-item').text(), I get "This is some textFirst span textSecond span text".

Is there a way to get (and possibly remove, via something like .text("")) just the free text within a tag, and not the text within its child tags?

The HTML was not written by me, so this is what I have to work with. I know that it would be simple to just wrap the text in tags when writing the html, but again, the html is pre-written.

Because I do not have enough reputation yet to comment and I do not wish the knowledge to be lost (hopefully it helps someone else), a combination of macio.Jun' answer, a RegExp, and iStranger' answer to Replace a textNode with HTML in Javascript? allowed me to search text-only nodes for a string and replace all occurrences with links.

J
Jonathan Arbely

I liked this reusable implementation based on the clone() method found here to get only the text inside the parent element.

Code provided for easy reference:

$("#foo")
    .clone()    //clone the element
    .children() //select all the children
    .remove()   //remove all the children
    .end()  //again go back to selected element
    .text();

With this solution you only get the text without the child, but you can't replace only the text.
I don't get 1 thing: If .end() goes back to selected element, than text() should copy original text with children elements. But in practice I see that text from our manipulated clone is being copied. So end() goes back to clone() ?
This is a really inefficient way of doing this
@billyonecan, can you suggest a more efficient method? This is appealing because it is "clean" and "short". What do you suggest?
@derekmx271 have a look at Stuart's answer
M
Mrchief

Simple answer:

$("#listItem").contents().filter(function(){ 
  return this.nodeType == 3; 
})[0].nodeValue = "The text you want to replace with" 

I don't understand why efficient answers (that dont generate extraneous data structures) are not voted up as much as answers that look less scary. +5 if I could.
the simple and efficient answer
This is not only more efficient but also correct! This solution caters for situations when the text is scattered between child elements. +5
To be even clearer, if you use IE8+, you can use this.nodeType == Node.TEXT_NODE instead of this.nodeType == 3. Easier to read and understand IMO.
This will break if you use it on something with no text. If you're using this as a function and have a scenario where you may or may not have text, just capture the .contents().filter(...) call into a local variable and check its length, e.g., var text = $(this).contents().filter(...); if (text.length) { return text[0].nodeValue; } return "";
T
TLS

This seems like a case of overusing jquery to me. The following will grab the text ignoring the other nodes:

document.getElementById("listItem").childNodes[0];

You'll need to trim that but it gets you what you want in one, easy line.

EDIT

The above will get the text node. To get the actual text, use this:

document.getElementById("listItem").childNodes[0].nodeValue;

Best answer, you're not supposed to need a plugin for this or a chain of 10 jQuery calls. $('.foo')[0].childNodes[0].nodeValue.trim()
what if the text content is split up into several nodes (like a sequence of crlf, text, crlf)? are there any (rael-life) guarantees that the dom constructed by the ua will use the simplest structure ?
Totally the best answer... why other people sometimes over use jQuery?
Maybe to avoid having to take care of cross browser compatibility?
This only works in the case of
text you wantother
. It will not work for
othertext you want
M
MadScientist

Easier and quicker:

$("#listItem").contents().get(0).nodeValue

Is this cross browser compatible ?
Of course it retrieves one of the elements matched by the jQuery object given by the index: Jquery Docs .get().
@Nate In case you need use it on a <br/> tag you could use the macio.Jun's answer.
Why get(0) instead of just [0]?
Fails on <div id="listItem"> <div>Contents?</div> Text? What text? </div>
D
DUzun

Similar to the accepted answer, but without cloning:

$("#foo").contents().not($("#foo").children()).text();

And here is a jQuery plugin for this purpose:

$.fn.immediateText = function() {
    return this.contents().not(this.children()).text();
};

Here is how to use this plugin:

$("#foo").immediateText(); // get the text without children

What is t in t.children()?
This is a duplicate solution of the one that pbjk wrote in Jan'15... nonetheless - it looks nice.
Not really, @Oskar. The .contents() part is critical here!
@AndroidDev You can always replace the selector with whatever works for you. This is just to illustrate the technique! I also added a Plugin version to show that it works even without IDs
You get my upvote for the jQuery plugin - a very elegant solution to tackle it in the most generic & usable way.
B
Brent

isn't the code:

var text  =  $('#listItem').clone().children().remove().end().text();

just becoming jQuery for jQuery's sake? When simple operations involve that many chained commands & that much (unnecessary) processing, perhaps it is time to write a jQuery extension:

(function ($) {
    function elementText(el, separator) {
        var textContents = [];
        for(var chld = el.firstChild; chld; chld = chld.nextSibling) {
            if (chld.nodeType == 3) { 
                textContents.push(chld.nodeValue);
            }
        }
        return textContents.join(separator);
    }
    $.fn.textNotChild = function(elementSeparator, nodeSeparator) {
    if (arguments.length<2){nodeSeparator="";}
    if (arguments.length<1){elementSeparator="";}
        return $.map(this, function(el){
            return elementText(el,nodeSeparator);
        }).join(elementSeparator);
    }
} (jQuery));

to call:

var text = $('#listItem').textNotChild();

the arguments are in case a different scenario is encountered, such as

<li>some text<a>more text</a>again more</li>
<li>second text<a>more text</a>again more</li>

var text = $("li").textNotChild(".....","<break>");

text will have value:

some text<break>again more.....second text<break>again more

Nice. How about making this a pull request for the next version of jQuery?
p
pbjk

Try this:

$('#listItem').not($('#listItem').children()).text()

This looks simply wrong.
佚名

It'll need to be something tailored to the needs, which are dependent on the structure you're presented with. For the example you've provided, this works:

$(document).ready(function(){
     var $tmp = $('#listItem').children().remove();
     $('#listItem').text('').append($tmp);
});

Demo: http://jquery.nodnod.net/cases/2385/run

But it's fairly dependent on the markup being similar to what you posted.


Future reader beware: the code in this answer kills the children in the actual element. One should use the clone method here if that's not the intended effect.
@DotNetWala's answer, below, and should be used instead of this one. Or at the least, use the .detach() method instead of .remove().
g
galeksandrp
$($('#listItem').contents()[0]).text()

Short variant of Stuart answer.

or with get()

$($('#listItem').contents().get(0)).text()

B
Brave Dolphin
jQuery.fn.ownText = function () {
    return $(this).contents().filter(function () {
        return this.nodeType === Node.TEXT_NODE;
    }).text();
};

Thank you for this code snippet, which may provide some immediate help. A proper explanation would greatly improve its educational value by showing why this is a good solution to the problem, and would make it more useful to future readers with similar, but not identical, questions. Please edit your answer to add explanation, and give an indication of what limitations and assumptions apply.
i
inarilo

If the position index of the text node is fixed among its siblings, you can use

$('parentselector').contents().eq(index).text()

r
rotaercz

This is an old question but the top answer is very inefficient. Here's a better solution:

$.fn.myText = function() {
    var str = '';

    this.contents().each(function() {
        if (this.nodeType == 3) {
            str += this.textContent || this.innerText || '';
        }
    });

    return str;
};

And just do this:

$("#foo").myText();

m
mvmn

I presume this would be a fine solution also - if you want to get contents of all text nodes that are direct children of selected element.

$(selector).contents().filter(function(){ return this.nodeType == 3; }).text();

Note: jQuery documentation uses similar code to explain contents function: https://api.jquery.com/contents/

P.S. There's also a bit uglier way to do that, but this shows more in depth how things work, and allows for custom separator between text nodes (maybe you want a line break there)

$(selector).contents().filter(function(){ return this.nodeType == 3; }).map(function() { return this.nodeValue; }).toArray().join("");

g
gaetanoM

I propose to use the createTreeWalker to find all texts elements not attached to html elements (this function can be used to extend jQuery):

function textNodesOnlyUnder(el) { var resultSet = []; var n = null; var treeWalker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, function (node) { if (node.parentNode.id == el.id && node.textContent.trim().length != 0) { return NodeFilter.FILTER_ACCEPT; } return NodeFilter.FILTER_SKIP; }, false); while (n = treeWalker.nextNode()) { resultSet.push(n); } return resultSet; } window.onload = function() { var ele = document.getElementById('listItem'); var textNodesOnly = textNodesOnlyUnder(ele); var resultingText = textNodesOnly.map(function(val, index, arr) { return 'Text element N. ' + index + ' --> ' + val.textContent.trim(); }).join('\n'); document.getElementById('txtArea').value = resultingText; }

  • This is some text First span text Second span text

  • M
    Marcus Parsons

    Using plain JavaScript in IE 9+ compatible syntax in just a few lines:

    const childNodes = document.querySelector('#listItem').childNodes;
    
    if (childNodes.length > 0) {
        childNodesLoop:
        for (let i = 0; i < childNodes.length; i++) {
            //only target text nodes (nodeType of 3)
            if (childNodes[i].nodeType === 3) {
                //do not target any whitespace in the HTML
                if (childNodes[i].nodeValue.trim().length > 0) {
                    childNodes[i].nodeValue = 'Replacement text';
                    //optimized to break out of the loop once primary text node found
                    break childNodesLoop;
                }
            }
        }
    }
    

    A couple of notes: I wouldn't call that variable children, because the children property of an element is a list of its child elements. I'd use childNodes to avoid confusion. Also, there's no need for the label, there's just the one loop, break will break the right thing. Also, the "primary text" thing seems to come out of left field, the OP doesn't mention stopping early for any reason.
    The variable renaming sounds good, but I used a label for the for loop just because I like to see which loop I'm breaking especially when there's text in between. It doesn't hurt performance to label the for loop. And in the original post, it said "a text node" which makes anyone infer that there is only one element being targeted in that list. It is faster to break if there's only one possibility to replace.
    As you say, no harm in labelling it for clarity. :-)
    M
    Mif.ComicVN

    This is a good way for me

       var text  =  $('#listItem').clone().children().remove().end().text();
    

    This is exactly the same as DotNetWala's answer.
    Y
    Yu Jiang Tham

    I came up with a specific solution that should be much more efficient than the cloning and modifying of the clone. This solution only works with the following two reservations, but should be more efficient than the currently accepted solution:

    You are getting only the text The text you want to extract is before the child elements

    With that said, here is the code:

    // 'element' is a jQuery element
    function getText(element) {
      var text = element.text();
      var childLength = element.children().text().length;
      return text.slice(0, text.length - childLength);
    }
    

    J
    Jeach

    Just like the question, I was trying to extract text in order to do some regex substitution of the text but was getting problems where my inner elements (ie: <i>, <div>, <span>, etc.) were getting also removed.

    The following code seems to work well and solved all my problems.

    It uses some of the answers provided here but in particular, will only substitute the text when the element is of nodeType === 3.

    $(el).contents().each(function() { 
      console.log(" > Content: %s [%s]", this, (this.nodeType === 3));
    
      if (this.nodeType === 3) {
        var text = this.textContent;
        console.log(" > Old   : '%s'", text);
    
        regex = new RegExp("\\[\\[" + rule + "\\.val\\]\\]", "g");
        text = text.replace(regex, value);
    
        regex = new RegExp("\\[\\[" + rule + "\\.act\\]\\]", "g");
        text = text.replace(regex, actual);
    
        console.log(" > New   : '%s'", text);
        this.textContent = text;
      }
    });
    

    What the above does is loop through all the elements of the given el (which was simply obtained with $("div.my-class[name='some-name']");. For each inner element, it basically ignores them. For each portion of text (as determined by if (this.nodeType === 3)) it will apply the regex substitution only to those elements.

    The this.textContent = text portion simply replaces the substituted text, which in my case, I was looking for tokens like [[min.val]], [[max.val]], etc.

    This short code excerpt will help anyone trying to do what the question was asking ... and a bit more.


    O
    OG Sean

    Not sure how flexible or how many cases you need it to cover, but for your example, if the text always comes before the first HTML tags – why not just split the inner html at the first tag and take the former:

    $('#listItem').html().split('<span')[0]; 
    

    and if you need it wider maybe just

    $('#listItem').html().split('<')[0]; 
    

    and if you need the text between two markers, like after one thing but before another, you can do something like (untested) and use if statements to make it flexible enough to have a start or end marker or both, while avoiding null ref errors:

    var startMarker = '';// put any starting marker here
    var endMarker = '<';// put the end marker here
    var myText = String( $('#listItem').html() );
    // if the start marker is found, take the string after it
    myText = myText.split(startMarker)[1];        
    // if the end marker is found, take the string before it
    myText = myText.split(endMarker)[0];
    console.log(myText); // output text between the first occurrence of the markers, assuming both markers exist.  If they don't this will throw an error, so some if statements to check params is probably in order...
    

    I generally make utility functions for useful things like this, make them error free, and then rely on them frequently once solid, rather than always rewriting this type of string manipulation and risking null references etc. That way, you can re-use the function in lots of projects and never have to waste time on it again debugging why a string reference has an undefined reference error. Might not be the shortest 1 line code ever, but after you have the utility function, it is one line from then on. Note most of the code is just handling parameters being there or not to avoid errors :)

    For example:

    /**
    * Get the text between two string markers.
    **/
    function textBetween(__string,__startMark,__endMark){
        var hasText = typeof __string !== 'undefined' && __string.length > 0;
        if(!hasText) return __string;
        var myText = String( __string );
        var hasStartMarker = typeof __startMark !== 'undefined' && __startMark.length > 0 && __string.indexOf(__startMark)>=0;
        var hasEndMarker =  typeof __endMark !== 'undefined' && __endMark.length > 0 && __string.indexOf(__endMark) > 0;
        if( hasStartMarker )  myText = myText.split(__startMark)[1];
        if( hasEndMarker )    myText = myText.split(__endMark)[0];
        return myText;
    }
    
    // now with 1 line from now on, and no jquery needed really, but to use your example:
    var textWithNoHTML = textBetween( $('#listItem').html(), '', '<'); // should return text before first child HTML tag if the text is on page (use document ready etc)
    

    if you need to replace text, just use $('#listItem').html( newHTML ); where newHTML is a variable that already has the stripped down text.
    D
    Dexter

    Live demo

    <li id="listItem">
        This is some text
        <span id="firstSpan">First span text</span>
        <span id="secondSpan">Second span text</span>
    </li>
    
    <input id="input" style="width: 300px; margin-top: 10px;">
    
        <script type="text/javascript">
    $("#input").val($("#listItem").clone().find("span").remove().end().text().trim());
        //use .trim() to remove any white space
        </script>
    

    D
    Dh. Yaduvanshi

    For beginners:

    I preferred @DUzun's answer because it's simple to understand and more efficient than the accepted answer. But it only partially worked for me as you can't directly pass the element with a class selector like this

    $(".landing-center .articlelanding_detail").get(0).immediateText() //gives .immediateText is not a function error
    

    or this

    $(".landing-center .articlelanding_detail")[0].immediateText() //gives .immediateText is not a function error
    

    because once you extract the native Element by using [index] or .get(index) out of the $() function you loose jQuery Object methods chainability as mentioned here. And most of the solutions are only in context to ids, not so elegant to use multiple times for the elements with a class selectors.

    So, I wrote jQuery plugin:

    $.fn.mainText = function(x=0) {
        return $.trim(this.eq(x).contents().not(this.eq(x).children()).text().replace(/[\t\n]+/g,' '));
    };
    

    This will return the text of the element irrespective of if ids or class are used as selectors excluding child elements. Also will remove any \t or \n to get a clean string. Use it like this:

    Case 1

    $("#example").mainText(); // get the text of element with example id
    

    Case 2

    $(".example").mainText(); // get the text of first element with example class
    

    Case 3

    $(".example").mainText(1); // get the text of second element with example class and so on..
    

    T
    T.J. Crowder

    I wouldn't bother with jQuery for this, especially not the solutions that make unnecessary clones of the elements. A simple loop grabbing text nodes is all you need. In modern JavaScript (as of this writing — "modern" is a moving target!) and trimming whitespace from the beginning and end of the result:

    const { childNodes } = document.getElementById("listItem");
    let text = "";
    for (const node of childNodes) {
        if (node.nodeType === Node.TEXT_NODE) {
            text += node.nodeValue;
        }
    }
    text = text.trim();
    

    Live Example:

    const { childNodes } = document.getElementById("listItem"); let text = ""; for (const node of childNodes) { if (node.nodeType === Node.TEXT_NODE) { text += node.nodeValue; } } console.log(text);

  • This is some text First span text Second span text
  • Some people would use reduce for this. I'm not a fan, I think a simple loop is clearer, but this usage does update the accumulator on each iteration, so it's not actually abusing reduce:

    const { childNodes } = document.getElementById("listItem");
    const text = [...childNodes].reduce((text, node) =>
        node.nodeType === Node.TEXT_NODE ? text + node.nodeValue : text
    , "").trim();
    

    const { childNodes } = document.getElementById("listItem"); const text = [...childNodes].reduce((text, node) => node.nodeType === Node.TEXT_NODE ? text + node.nodeValue : text , "").trim(); console.log(text);

  • This is some text First span text Second span text
  • Or without creating a temporary array:

    const { childNodes } = document.getElementById("listItem");
    const text = Array.prototype.reduce.call(childNodes, (text, node) =>
        node.nodeType === Node.TEXT_NODE ? text + node.nodeValue : text
    , "").trim();
    

    const { childNodes } = document.getElementById("listItem"); const text = Array.prototype.reduce.call(childNodes, (text, node) => node.nodeType === Node.TEXT_NODE ? text + node.nodeValue : text , "").trim(); console.log(text);

  • This is some text First span text Second span text

  • P
    Paul Verschoor

    Use an extra condition to check if innerHTML and innerText are the same. Only in those cases, replace the text.

    $(function() {
    $('body *').each(function () {
        console.log($(this).html());
        console.log($(this).text());
        if($(this).text() === "Search" && $(this).html()===$(this).text())  {
            $(this).html("Find");
        }
    })
    })
    

    http://jsfiddle.net/7RSGh/


    M
    Marion Go

    To be able to trim the result, use DotNetWala's like so:

    $("#foo")
        .clone()    //clone the element
        .children() //select all the children
        .remove()   //remove all the children
        .end()  //again go back to selected element
        .text()
        .trim();
    

    I found out that using the shorter version like document.getElementById("listItem").childNodes[0] won't work with jQuery's trim().


    That's because document.getElementById("listItem").childNodes[0] is plain javascript, you'd have to wrap it in the jQuery function $(document.getElementById("listItem").childNodes[0]).trim()
    Okay that makes sense. Haha. Thanks!
    This is nearly identical to DotNetWala's answer. All you did was added .trim() to the end. Is this answer necessary?
    D
    Dorjan

    just put it in a <p> or <font> and grab that $('#listItem font').text()

    First thing that came to mind

    <li id="listItem">
        <font>This is some text</font>
        <span id="firstSpan">First span text</span>
        <span id="secondSpan">Second span text</span>
    </li>
    

    I don't have control over putting the free text in tags, because the code I'm working off of was not created by me. If I could grab just that text, I could remove it and replace it with tags around it, or do anything I want. But again, the html is already pre-written.
    ah, ok. Then I think you're going to have to filter the results :S sorry.
    a
    achakravarty

    You can try this

    alert(document.getElementById('listItem').firstChild.data)
    

    S
    Sudheera

    I am not a jquery expert, but how about,

    $('#listItem').children().first().text()
    

    If you're note a jquery expert, then why not become more of an expert by reading through the other answers first?... One of them happened to be virtually the same as what you wrote, with comments below that explain why it's not a good idea.
    E
    El Guapo

    This untested, but I think you may be able to try something like this:

     $('#listItem').not('span').text();
    

    http://api.jquery.com/not/


    Because it's the same as $('#listItem').text(). #listItem isn't a <span> so adding not('span') doesn't do anything.