Jak zdobyć węzeł tekstowy elementu?

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

Chcę otrzymać opcję „Jestem węzłem tekstowym”, nie chcę usuwać tagu „edytuj” i potrzebuję rozwiązania obsługującego wiele przeglądarek.

Val
źródło
to pytanie jest prawie identyczne ze stackoverflow.com/questions/3172166/ ... - zobacz te odpowiedzi dla zwykłej wersji JS odpowiedzi Jamesa
Mała

Odpowiedzi:

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

Pobiera element contentsz wybranego elementu i stosuje do niego funkcję filtrującą. Funkcja filtru zwraca tylko węzły tekstowe (tj. Te z nodeType == Node.TEXT_NODE).

James Allardice
źródło
@Val - przepraszam, przegapiłem to z oryginalnego kodu. Zaktualizuję odpowiedź, aby ją pokazać. Potrzebujesz, text()ponieważ filterfunkcja zwraca same węzły, a nie zawartość węzłów.
James Allardice
1
Nie wiem, dlaczego, ale nie udaje mi się testować powyższej teorii. Wykonałem następujące czynności jQuery("*").each(function() { console.log(this.nodeType); })i otrzymałem 1 dla wszystkich typów węzłów.
Batandwa,
Czy można uzyskać tekst w klikniętym węźle i tekst we wszystkich jego elementach potomnych?
Jenna Kwon
Jest to interesujące i rozwiązuje ten problem, ale co się stanie, gdy sytuacja stanie się bardziej złożona? Istnieje bardziej elastyczny sposób wykonania zadania.
Anthony Rutledge
Bez jQuery, document.querySelector (". Title"). ChildNodes [0] .nodeValue
Balaji Gunasekaran
57

Możesz uzyskać wartość nodeValue pierwszego childNode za pomocą

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

http://jsfiddle.net/TU4FB/

Dogbert
źródło
4
Chociaż to zadziała, zależy to od położenia węzłów podrzędnych. Jeśli (kiedy) to się zmieni, pęknie.
Armstrongest
Jeśli węzeł tekstowy nie jest pierwszym dzieckiem, możesz otrzymać nullwartość zwracaną.
Anthony Rutledge
15

Jeśli masz na myśli pobranie wartości pierwszego węzła tekstowego w elemencie, ten kod zadziała:

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;
    }
}

Możesz to zobaczyć w akcji tutaj: http://jsfiddle.net/ZkjZJ/

Czarodziej cienia to ucho dla Ciebie
źródło
Myślę, że możesz użyć curNode.nodeType == 3zamiast tego nodeName.
Nilloc
1
@Nilloc prawdopodobnie, ale jaki jest zysk?
Shadow Wizard is Ear For You
5
@ShadowWizard @Nilloc zalecanym sposobem na to jest użycie stałych ... curNode.nodeType == Node.TEXT_NODE(porównanie liczbowe jest szybsze, ale curNode.nodeType == 3 jest nieczytelne - który węzeł ma numer 3?)
mikep
1
@ShadowWizard Use curNode.NodeType === Node.TEXT_NODE. To porównanie zachodzi w pętli nieznanych możliwych iteracji. Porównywanie dwóch małych liczb jest lepsze niż porównywanie łańcuchów o różnych długościach (względy czasu i przestrzeni). Prawidłowe pytanie, które należy zadać w tej sytuacji, to „jaki mam rodzaj / typ węzła?”, A nie „jaką mam nazwę?” developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
Anthony Rutledge
2
@ShadowWizard Ponadto, jeśli zamierzasz używać pętli do przeszukiwania childNodes, wiedz, że węzeł elementu może mieć więcej niż jeden węzeł tekstowy. W rozwiązaniu ogólnym może być konieczne określenie, do której instancji węzła tekstowego w węźle elementu chcesz kierować (pierwszą, drugą, trzecią itd.).
Anthony Rutledge
14

Innym natywnym rozwiązaniem JS, które może być przydatne w przypadku „złożonych” lub głęboko zagnieżdżonych elementów, jest użycie NodeIterator . Umieść NodeFilter.SHOW_TEXTjako drugi argument („whatToShow”) i przeprowadź iterację tylko po elementach potomnych węzła tekstowego.

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>

Możesz także użyć TreeWalker. Różnica między nimi polega na tym, że NodeIteratorjest prostym liniowym iteratorem, a jednocześnie TreeWalkerumożliwia nawigację za pośrednictwem rodzeństwa i przodków.

Yuval A.
źródło
9

Czysty JavaScript: minimalistyczny

Po pierwsze, zawsze miej to na uwadze, szukając tekstu w DOM.

MDN - odstępy w DOM

Ta kwestia sprawi, że zwrócisz uwagę na strukturę swojego XML / HTML.

W tym czystym przykładzie JavaScript uwzględniam możliwość wielu węzłów tekstowych, które można przeplatać z innymi rodzajami węzłów . Jednak początkowo nie oceniam białych znaków, pozostawiając to zadanie filtrowania innemu kodowi.

W tej wersji przekazuję NodeListkod wywołujący / klienta.

/**
* 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;
}

Oczywiście, testując node.hasChildNodes()najpierw, nie byłoby potrzeby używania forpętli przed testem .

/**
* 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;
}

Czysty JavaScript: solidny

Tutaj funkcja getTextById()używa dwóch funkcji pomocniczych: getStringsFromChildren()i 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.
}

Następnie wartość zwracana (Array lub null) jest wysyłana do kodu klienta, w którym ma być obsługiwana. Miejmy nadzieję, że tablica powinna zawierać elementy tekstowe prawdziwego tekstu, a nie wiersze odstępów.

Puste ciągi ( "") nie są zwracane, ponieważ potrzebny jest węzeł tekstowy do prawidłowego wskazania obecności prawidłowego tekstu. Zwracanie ( "") może dawać fałszywe wrażenie, że istnieje węzeł tekstowy, co prowadzi do założenia, że ​​może zmienić tekst, zmieniając wartość .nodeValue. To jest fałsz, ponieważ węzeł tekstowy nie istnieje w przypadku pustego ciągu.

Przykład 1 :

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

Przykład 2 :

<p id="bio">

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

Problem pojawia się, gdy chcesz, aby kod HTML był łatwy do odczytania, rozstawiając go. Teraz, mimo że nie ma poprawnego tekstu czytelnego dla człowieka, nadal istnieją węzły tekstowe ze "\n"znakami nowej linii ( ) we .nodeValuewłaściwościach.

Ludzie widzą przykłady pierwszy i drugi jako równoważne funkcjonalnie - puste elementy czekające na wypełnienie. DOM różni się od ludzkiego rozumowania. Dlatego getStringsFromChildren()funkcja musi określić, czy istnieją węzły tekstowe i zebrać .nodeValuewartości w tablicy.

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

W przykładzie dwa istnieją dwa węzły tekstowe i getStringFromChildren()zwrócą wartość .nodeValueobu z nich ( "\n"). Jednak filterWhitespaceLines()używa wyrażenia regularnego, aby odfiltrować wiersze zawierające czyste białe znaki.

Czy powrót nullzamiast "\n"znaków nowej linii ( ) jest formą okłamywania klienta / kodu wywołującego? W kategoriach ludzkich nie. W kategoriach DOM, tak. Jednak problemem jest tutaj pobranie tekstu, a nie jego edycja. Nie ma tekstu, który mógłby powrócić do kodu wywołującego.

Nigdy nie wiadomo, ile znaków nowej linii może pojawić się w czyimś kodzie HTML. Tworzenie licznika szukającego „drugiego” znaku nowej linii jest zawodne. Może nie istnieć.

Oczywiście w dalszej części linii, kwestia edycji tekstu w pustym <p></p>elemencie z dodatkowymi białymi znakami (przykład 2) może oznaczać zniszczenie (być może przeskoczenie) wszystkich węzłów tekstowych z wyjątkiem jednego między znacznikami akapitu, aby upewnić się, że element zawiera dokładnie to, czym jest powinien wyświetlić.

Niezależnie od tego, z wyjątkiem przypadków, w których robisz coś niezwykłego, będziesz potrzebować sposobu na określenie, która .nodeValuewłaściwość węzła tekstowego zawiera prawdziwy, czytelny dla człowieka tekst, który chcesz edytować. filterWhitespaceLinesprowadzi nas w połowie drogi.

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. 
    }
}

W tym momencie możesz otrzymać wynik, który wygląda następująco:

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

Nie ma gwarancji, że te dwa ciągi są ze sobą sąsiadujące w DOM, więc połączenie ich .join()może stworzyć nienaturalny kompozyt. Zamiast tego w kodzie, który wywołujegetTextById() musisz wybrać ciąg, z którym chcesz pracować.

Przetestuj wyjście.

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);
}

Można dodać .trim()wewnątrz, getStringsFromChildren()aby pozbyć się wiodących i końcowych białych znaków (lub zamienić kilka spacji w ciąg znaków o zerowej długości ( ""), ale skąd można wiedzieć a priori, co każda aplikacja może musieć się przydarzyć tekstowi (łańcuchowi) gdy zostanie znaleziony? Nie możesz, więc zostaw to konkretnej implementacji i niech getStringsFromChildren()będzie ogólne.

Może się zdarzyć, że ten poziom szczegółowości ( targeti taki) nie będzie wymagany. To wspaniale. W takich przypadkach użyj prostego rozwiązania. Jednak uogólniony algorytm umożliwia dostosowanie się do prostych i złożonych sytuacji.

Anthony Rutledge
źródło
8

Wersja ES6, która zwraca pierwszą zawartość węzła #text

const extract = (node) => {
  const text = [...node.childNodes].find(child => child.nodeType === Node.TEXT_NODE);
  return text && text.textContent.trim();
}
jujule
źródło
Zastanawiam się nad wydajnością i elastycznością. (1) Użycie .from()do tworzenia płytkiej kopii tablicy. (2) Użycie .find()do wykonania porównań ciągów przy użyciu .nodeName. Używanie node.NodeType === Node.TEXT_NODEbyłoby lepsze. (3) Zwracanie pustego ciągu, gdy nie ma wartości null, jest bardziej prawdziwe, jeśli nie zostanie znaleziony węzeł tekstowy . Jeśli nie zostanie znaleziony żaden węzeł tekstowy, może być konieczne jego utworzenie! Jeśli zwrócisz pusty ciąg, ""możesz sprawić fałszywe wrażenie, że istnieje węzeł tekstowy i można nim normalnie manipulować. W istocie zwracanie pustego ciągu jest białym kłamstwem i najlepiej go unikać.
Anthony Rutledge
(4) Jeśli w nodeList jest więcej niż jeden węzeł tekstowy, nie ma możliwości określenia, który węzeł tekstowy chcesz. Możesz potrzebować pierwszego węzła tekstowego, ale bardzo dobrze możesz chcieć ostatniego węzła tekstowego.
Anthony Rutledge
Co sugerujesz, aby zastąpić Array.from?
jujule
@Snowman, dodaj własną odpowiedź na takie istotne zmiany lub przedstaw zalecenia dla OP, aby dać im możliwość uwzględnienia ich w swojej odpowiedzi.
TylerH
@jujule - Lepsze w użyciu [...node.childNodes]do konwersji HTMLCollection na tablice
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
źródło
1
Myślę, że metodą standardowego javascript musi być „innerText”
Reporter
2
To nie działa tak, jak chce OP - otrzyma również tekst w aelemencie: jsfiddle.net/ekHJH
James Allardice
1
@James Allardice - Skończyłem z rozwiązaniem jquery, teraz to zadziała .................
Pranay Rana
To prawie zadziała, ale brakuje ci .na początku selektora, co oznacza, że ​​faktycznie otrzymujesz tekst titleelementu, a nie elementy zclass="title"
James Allardice.
@reporter .innerTextto stara konwencja IE, przyjęta dopiero niedawno. Jeśli chodzi o standardowe skrypty DOM, node.nodeValuejest to sposób pobierania tekstu węzła tekstowego.
Anthony Rutledge
2

Spowoduje to również zignorowanie białych znaków, więc nigdy nie otrzymałeś pustego kodu textNodes… przy użyciu podstawowego kodu JavaScript.

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;
    }
}

Sprawdź na jsfiddle: - http://jsfiddle.net/webx/ZhLep/

webx
źródło
curNode.nodeType === Node.TEXT_NODEbyłoby lepiej. Używanie porównania ciągów i wyrażenia regularnego w pętli jest rozwiązaniem o niskiej wydajności, zwłaszcza gdy wielkość oDiv.childNodes.lengthwzrasta. Algorytm ten rozwiązuje specyficzne pytanie OP, ale potencjalnie przy strasznych kosztach wydajności. Jeśli układ lub liczba węzłów tekstowych ulegnie zmianie, nie można zagwarantować, że to rozwiązanie zwróci dokładne wyniki. Innymi słowy, nie możesz wskazać konkretnego węzła tekstowego, który chcesz. Jesteś na łasce struktury HTML i układu tekstu w tym miejscu.
Anthony Rutledge
1

Możesz również użyć text()testu węzłów XPath, aby uzyskać tylko węzły tekstowe. Na przykład

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;
}
doubleDown
źródło
0

To jest moje rozwiązanie w ES6, aby utworzyć ciąg zawierający połączony tekst wszystkich węzłów potomnych (rekurencyjny) . Zauważ, że jest to również wizyta w shdowroot of 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');
}

To rozwiązanie zostało zainspirowane rozwiązaniem https://stackoverflow.com/a/41051238./1300775 .

Damien
źródło