Como obter o nó de texto de um elemento?

98
<div class="title">
   I am text node
   <a class="edit">Edit</a>
</div>

Desejo obter o "I am text node", não desejo remover a tag "edit" e preciso de uma solução para vários navegadores.

Val
fonte
esta pergunta é praticamente idêntica a stackoverflow.com/questions/3172166/… - veja essas respostas para uma versão JS simples da resposta de James
Mala

Respostas:

79
var text = $(".title").contents().filter(function() {
  return this.nodeType == Node.TEXT_NODE;
}).text();

Isso obtém o contentsdo elemento selecionado e aplica uma função de filtro a ele. A função de filtro retorna apenas nós de texto (ou seja, aqueles nós com nodeType == Node.TEXT_NODE).

James Allardice
fonte
@Val - desculpe, eu perdi o código original. Vou atualizar a resposta para mostrar isso. Você precisa text()porque a filterfunção retorna os próprios nós, não o conteúdo dos nós.
James Allardice
1
Não sei por que, mas não tenho sucesso ao testar a teoria acima. Executei o seguinte jQuery("*").each(function() { console.log(this.nodeType); })e obtive 1 para todos os tipos de nó.
Batandwa
É possível obter texto no nó clicado e texto em todos os seus filhos?
Jenna Kwon
Isso é interessante e resolve o problema, mas o que acontece quando a situação fica mais complexa? Existe uma maneira mais flexível de realizar o trabalho.
Anthony Rutledge
Sem jQuery, document.querySelector (". Title"). ChildNodes [0] .nodeValue
Balaji Gunasekaran
53

Você pode obter o nodeValue do primeiro childNode usando

$('.title')[0].childNodes[0].nodeValue

http://jsfiddle.net/TU4FB/

Dogbert
fonte
4
Embora isso funcione, depende da posição dos nós filhos. Se (quando) isso mudar, vai quebrar.
Armstrongest
Se o nó de texto não for o primeiro filho, você pode obter nullum valor de retorno.
Anthony Rutledge
14

Se você quiser obter o valor do primeiro nó de texto no elemento, este código funcionará:

var oDiv = document.getElementById("MyDiv");
var firstText = "";
for (var i = 0; i < oDiv.childNodes.length; i++) {
    var curNode = oDiv.childNodes[i];
    if (curNode.nodeName === "#text") {
        firstText = curNode.nodeValue;
        break;
    }
}

Você pode ver isso em ação aqui: http://jsfiddle.net/ZkjZJ/

Shadow Wizard é ouvido para você
fonte
Acho que você também poderia usar em curNode.nodeType == 3vez de nodeName.
Nilloc
1
@Nilloc provavelmente, mas qual é o ganho?
Shadow Wizard is Ear For You
5
A maneira recomendada de @ShadowWizard @Nilloc para isso é usar constantes ... curNode.nodeType == Node.TEXT_NODE(a comparação numérica é mais rápida, mas curNode.nodeType == 3 não é legível - qual nó tem o número 3?)
mikep
1
@ShadowWizard Use curNode.NodeType === Node.TEXT_NODE. Essa comparação está ocorrendo dentro de um loop de possíveis iterações desconhecidas. Comparar dois números pequenos é melhor do que comparar strings de vários comprimentos (considerações de tempo e espaço). A pergunta correta a fazer nessa situação é "que tipo / tipo de nó eu tenho?", E não "que nome eu tenho?" developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
Anthony Rutledge
2
@ShadowWizard Além disso, se você for usar um loop para peneirar childNodes, saiba que um nó de elemento pode ter mais de um nó de texto. Em uma solução genérica, pode ser necessário especificar qual instância de um nó de texto dentro de um nó de elemento que você deseja atingir (o primeiro, segundo, terceiro, etc ...).
Anthony Rutledge
13

Outra solução JS nativa que pode ser útil para elementos "complexos" ou profundamente aninhados é usar NodeIterator . Coloque NodeFilter.SHOW_TEXTcomo o segundo argumento ("whatToShow") e itere apenas sobre os filhos do nó de texto do elemento.

var root = document.querySelector('p'),
    iter = document.createNodeIterator(root, NodeFilter.SHOW_TEXT),
    textnode;

// print all text nodes
while (textnode = iter.nextNode()) {
  console.log(textnode.textContent)
}
<p>
<br>some text<br>123
</p>

Você também pode usar TreeWalker. A diferença entre os dois é que NodeIteratoré um iterador linear simples, enquanto TreeWalkerpermite que você navegue por meio de irmãos e ancestrais também.

Yuval A.
fonte
9

JavaScript puro: minimalista

Em primeiro lugar, sempre tenha isso em mente ao procurar por texto no DOM.

MDN - espaço em branco no DOM

Este problema fará com que você preste atenção na estrutura do seu XML / HTML.

Neste exemplo de JavaScript puro, considero a possibilidade de vários nós de texto que podem ser intercalados com outros tipos de nós . No entanto, inicialmente, não julgo os espaços em branco, deixando essa tarefa de filtragem para outro código.

Nesta versão, passo um NodeListdo código de chamada / cliente.

/**
* Gets strings from text nodes. Minimalist. Non-robust. Pre-test loop version.
* Generic, cross platform solution. No string filtering or conditioning.
*
* @author Anthony Rutledge
* @param nodeList The child nodes of a Node, as in node.childNodes.
* @param target A positive whole number >= 1
* @return String The text you targeted.
*/
function getText(nodeList, target)
{
    var trueTarget = target - 1,
        length = nodeList.length; // Because you may have many child nodes.

    for (var i = 0; i < length; i++) {
        if ((nodeList[i].nodeType === Node.TEXT_NODE) && (i === trueTarget)) {
            return nodeList[i].nodeValue;  // Done! No need to keep going.
        }
    }

    return null;
}

Obviamente, testando node.hasChildNodes()primeiro, não haveria necessidade de usar um forloop de pré-teste .

/**
* Gets strings from text nodes. Minimalist. Non-robust. Post-test loop version.
* Generic, cross platform solution. No string filtering or conditioning.
*
* @author Anthony Rutledge
* @param nodeList The child nodes of a Node, as in node.childNodes.
* @param target A positive whole number >= 1
* @return String The text you targeted.
*/
function getText(nodeList, target)
{
    var trueTarget = target - 1,
        length = nodeList.length,
        i = 0;

    do {
        if ((nodeList[i].nodeType === Node.TEXT_NODE) && (i === trueTarget)) {
            return nodeList[i].nodeValue;  // Done! No need to keep going.
         }

        i++;
    } while (i < length);

    return null;
}

JavaScript puro: robusto

Aqui, a função getTextById()usa duas funções auxiliares: getStringsFromChildren()e filterWhitespaceLines().


getStringsFromChildren ()

/**
* Collects strings from child text nodes.
* Generic, cross platform solution. No string filtering or conditioning.
*
* @author Anthony Rutledge
* @version 7.0
* @param parentNode An instance of the Node interface, such as an Element. object.
* @return Array of strings, or null.
* @throws TypeError if the parentNode is not a Node object.
*/
function getStringsFromChildren(parentNode)
{
    var strings = [],
        nodeList,
        length,
        i = 0;

    if (!parentNode instanceof Node) {
        throw new TypeError("The parentNode parameter expects an instance of a Node.");
    }

    if (!parentNode.hasChildNodes()) {
        return null; // We are done. Node may resemble <element></element>
    }

    nodeList = parentNode.childNodes;
    length = nodeList.length;

    do {
        if ((nodeList[i].nodeType === Node.TEXT_NODE)) {
            strings.push(nodeList[i].nodeValue);
         }

        i++;
    } while (i < length);

    if (strings.length > 0) {
        return strings;
    }

    return null;
}

filterWhitespaceLines ()

/**
* Filters an array of strings to remove whitespace lines.
* Generic, cross platform solution.
*
* @author Anthony Rutledge
* @version 6.0
* @param textArray a String associated with the id attribute of an Element.
* @return Array of strings that are not lines of whitespace, or null.
* @throws TypeError if the textArray param is not of type Array.
*/
function filterWhitespaceLines(textArray) 
{
    var filteredArray = [],
        whitespaceLine = /(?:^\s+$)/; // Non-capturing Regular Expression.

    if (!textArray instanceof Array) {
        throw new TypeError("The textArray parameter expects an instance of a Array.");
    }

    for (var i = 0; i < textArray.length; i++) {
        if (!whitespaceLine.test(textArray[i])) {  // If it is not a line of whitespace.
            filteredArray.push(textArray[i].trim());  // Trimming here is fine. 
        }
    }

    if (filteredArray.length > 0) {
        return filteredArray ; // Leave selecting and joining strings for a specific implementation. 
    }

    return null; // No text to return.
}

getTextById ()

/**
* Gets strings from text nodes. Robust.
* Generic, cross platform solution.
*
* @author Anthony Rutledge
* @version 6.0
* @param id A String associated with the id property of an Element.
* @return Array of strings, or null.
* @throws TypeError if the id param is not of type String.
* @throws TypeError if the id param cannot be used to find a node by id.
*/
function getTextById(id) 
{
    var textArray = null;             // The hopeful output.
    var idDatatype = typeof id;       // Only used in an TypeError message.
    var node;                         // The parent node being examined.

    try {
        if (idDatatype !== "string") {
            throw new TypeError("The id argument must be of type String! Got " + idDatatype);
        }

        node = document.getElementById(id);

        if (node === null) {
            throw new TypeError("No element found with the id: " + id);
        }

        textArray = getStringsFromChildren(node);

        if (textArray === null) {
            return null; // No text nodes found. Example: <element></element>
        }

        textArray = filterWhitespaceLines(textArray);

        if (textArray.length > 0) {
            return textArray; // Leave selecting and joining strings for a specific implementation. 
        }
    } catch (e) {
        console.log(e.message);
    }

    return null; // No text to return.
}

Em seguida, o valor de retorno (Array ou null) é enviado ao código do cliente onde deve ser tratado. Felizmente, a matriz deve ter elementos de string de texto real, não linhas de espaço em branco.

Strings vazias ( "") não são retornadas porque você precisa de um nó de texto para indicar corretamente a presença de texto válido. Returning ( "") pode dar a falsa impressão de que existe um nó de texto, levando alguém a supor que pode alterar o texto alterando o valor de .nodeValue. Isso é falso, porque um nó de texto não existe no caso de uma string vazia.

Exemplo 1 :

<p id="bio"></p> <!-- There is no text node here. Return null. -->

Exemplo 2 :

<p id="bio">

</p> <!-- There are at least two text nodes ("\n"), here. -->

O problema surge quando você deseja tornar seu HTML fácil de ler, espaçando-o. Agora, embora não haja texto válido legível por humanos, ainda existem nós de texto com "\n"caracteres newline ( ) em suas .nodeValuepropriedades.

Os humanos veem os exemplos um e dois como funcionalmente equivalentes - elementos vazios esperando para serem preenchidos. O DOM é diferente do raciocínio humano. É por isso que a getStringsFromChildren()função deve determinar se existem nós de texto e reunir os .nodeValuevalores em uma matriz.

for (var i = 0; i < length; i++) {
    if (nodeList[i].nodeType === Node.TEXT_NODE) {
            textNodes.push(nodeList[i].nodeValue);
    }
}

No exemplo dois, dois nós de texto existem e getStringFromChildren()retornarão o .nodeValuede ambos ( "\n"). No entanto, filterWhitespaceLines()usa uma expressão regular para filtrar linhas de caracteres de espaço em branco puros.

Retornar em nullvez de "\n"caracteres newline ( ) é uma forma de mentir para o cliente / código de chamada? Em termos humanos, não. Em termos de DOM, sim. No entanto, o problema aqui é obter texto, não editá-lo. Não há texto humano para retornar ao código de chamada.

Nunca se sabe quantos caracteres de nova linha podem aparecer no HTML de alguém. A criação de um contador que procura o caractere de nova linha "segundo" não é confiável. Pode não existir.

Claro, mais abaixo na linha, a questão de editar texto em um <p></p>elemento vazio com espaço em branco extra (exemplo 2) pode significar destruir (talvez, pular) todos, exceto um nó de texto entre as tags de um parágrafo para garantir que o elemento contenha precisamente o que é deveria exibir.

Independentemente disso, exceto nos casos em que você está fazendo algo extraordinário, você precisará encontrar uma maneira de determinar qual .nodeValuepropriedade do nó de texto possui o texto legível verdadeiro que deseja editar. filterWhitespaceLinesnos leva a meio caminho.

var whitespaceLine = /(?:^\s+$)/; // Non-capturing Regular Expression.

for (var i = 0; i < filteredTextArray.length; i++) {
    if (!whitespaceLine.test(textArray[i])) {  // If it is not a line of whitespace.
        filteredTextArray.push(textArray[i].trim());  // Trimming here is fine. 
    }
}

Neste ponto, você pode ter uma saída semelhante a esta:

["Dealing with text nodes is fun.", "Some people just use jQuery."]

Não há garantia de que essas duas strings sejam adjacentes uma à outra no DOM, portanto, juntá-las com .join()pode formar uma composição não natural. Em vez disso, no código que chamagetTextById() , você precisa escolher com qual string deseja trabalhar.

Teste a saída.

try {
    var strings = getTextById("bio");

    if (strings === null) {
        // Do something.
    } else if (strings.length === 1) {
        // Do something with strings[0]
    } else { // Could be another else if
        // Do something. It all depends on the context.
    }
} catch (e) {
    console.log(e.message);
}

Pode-se adicionar .trim()dentro de getStringsFromChildren()para se livrar dos espaços em branco à esquerda e à direita (ou para transformar um monte de espaços em uma string de comprimento zero ( ""), mas como você pode saber a priori o que cada aplicativo pode precisar que aconteça com o texto (string) uma vez que for encontrado? Você não, então deixe isso para uma implementação específica e deixe getStringsFromChildren()ser genérico.

Pode haver momentos em que esse nível de especificidade (o targete tal) não seja necessário. Isso é ótimo. Use uma solução simples nesses casos. No entanto, um algoritmo generalizado permite acomodar situações simples e complexas.

Anthony Rutledge
fonte
8

Versão ES6 que retorna o primeiro #text node content

const extract = (node) => {
  const text = [...node.childNodes].find(child => child.nodeType === Node.TEXT_NODE);
  return text && text.textContent.trim();
}
Jujule
fonte
Estou me perguntando sobre eficiência e flexibilidade. (1) O uso de .from()para fazer uma instância de array copiado superficialmente. (2) O uso de .find()para fazer comparações de strings usando .nodeName. Usar node.NodeType === Node.TEXT_NODEseria melhor. (3) Retornar uma string vazia quando nenhum valor,, nullé mais verdadeiro se nenhum nó de texto for encontrado. Se nenhum nó de texto for encontrado, pode ser necessário criar um! Se você retornar uma string vazia,, ""poderá dar a falsa impressão de que existe um nó de texto e pode ser manipulado normalmente. Em essência, devolver uma string vazia é uma mentira inocente e é melhor evitar.
Anthony Rutledge
(4) Se houver mais de um nó de texto em uma nodeList, não há como especificar qual nó de texto você deseja. Você pode querer o primeiro nó de texto, mas muito bem pode querer o último nó de texto.
Anthony Rutledge
O que você sugere para substituir o Array.from?
julho de
@Snowman, por favor, adicione sua própria resposta para tais mudanças substanciais ou faça recomendações para o OP para dar a eles a oportunidade de incorporá-las em sua resposta.
TylerH
@jujule - Melhor usar [...node.childNodes]para converter HTMLCollection em Arrays
vsync
5

.text() - for jquery

$('.title').clone()    //clone the element
.children() //select all the children
.remove()   //remove all the children
.end()  //again go back to selected element
.text();    //get the text of element
Pranay Rana
fonte
1
Acho que o método para javascript padrão deve ser 'innerText'
Reporter
2
Isso não funciona da maneira que o OP deseja - ele obterá o texto dentro do aelemento também: jsfiddle.net/ekHJH
James Allardice
1
@James Allardice - Eu terminei com a solução jquery agora isso vai funcionar .................
Pranay Rana
Isso quase funcionará, mas está faltando o .no início do seu seletor, o que significa que você realmente obtém o texto do titleelemento, não os elementos comclass="title"
James Allardice
@reporter .innerTexté uma antiga convenção do IE adotada recentemente. Em termos de script DOM padrão, node.nodeValueé como se pega o texto de um nó de texto.
Anthony Rutledge
2

Isso irá ignorar o espaço em branco também, portanto, você nunca obteve o código textNodes em branco usando o Javascript principal.

var oDiv = document.getElementById("MyDiv");
var firstText = "";
for (var i = 0; i < oDiv.childNodes.length; i++) {
    var curNode = oDiv.childNodes[i];
    whitespace = /^\s*$/;
    if (curNode.nodeName === "#text" && !(whitespace.test(curNode.nodeValue))) {
        firstText = curNode.nodeValue;
        break;
    }
}

Verifique em jsfiddle: - http://jsfiddle.net/webx/ZhLep/

webx
fonte
curNode.nodeType === Node.TEXT_NODEseria melhor. Usar comparação de string e uma expressão regular em um loop é uma solução de baixo desempenho, especialmente à medida que a magnitude do oDiv.childNodes.lengthaumento. Este algoritmo resolve a questão específica do OP, mas, potencialmente, a um custo de desempenho terrível. Se o arranjo, ou número, de nós de texto mudar, então esta solução não pode ser garantida para retornar uma saída precisa. Em outras palavras, você não pode direcionar o nó de texto exato que deseja. Você está à mercê da estrutura HTML e da organização do texto.
Anthony Rutledge
1

Você também pode usar o text()teste de nó do XPath para obter apenas os nós de texto. Por exemplo

var target = document.querySelector('div.title');
var iter = document.evaluate('text()', target, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE);
var node;
var want = '';

while (node = iter.iterateNext()) {
    want += node.data;
}
apostar
fonte
0

Esta é minha solução no ES6 para criar uma string contendo o texto concatenado de todos os childnodes (recursivo) . Observe que também é possível visitar o shdowroot de childnodes.

function text_from(node) {
    const extract = (node) => [...node.childNodes].reduce(
        (acc, childnode) => [
            ...acc,
            childnode.nodeType === Node.TEXT_NODE ? childnode.textContent.trim() : '',
            ...extract(childnode),
            ...(childnode.shadowRoot ? extract(childnode.shadowRoot) : [])],
        []);

    return extract(node).filter(text => text.length).join('\n');
}

Esta solução foi inspirada na solução de https://stackoverflow.com/a/41051238./1300775 .

Damien
fonte