Как получить текстовый узел элемента?

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

Я хочу получить "Я - текстовый узел", не хочу удалять тег "edit" и мне нужно кроссбраузерное решение.

Вал
источник
этот вопрос в значительной степени идентичен stackoverflow.com/questions/3172166/… - см. эти ответы для простой JS-версии ответа Джеймса
Мала

Ответы:

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

Это получает значение contentsвыбранного элемента и применяет к нему функцию фильтра. Функция фильтра возвращает только текстовые узлы (т.е. узлы с nodeType == Node.TEXT_NODE).

Джеймс Аллардис
источник
@Val - извините, я пропустил это в исходном коде. Я обновлю ответ, чтобы показать это. Вам это нужно, text()потому что filterфункция возвращает сами узлы, а не их содержимое.
Джеймс Аллардис,
1
Не уверен, почему, но мне не удалось проверить вышеизложенную теорию. Я выполнил следующее jQuery("*").each(function() { console.log(this.nodeType); })и получил 1 для всех типов узлов.
Batandwa 06
Можно ли получить текст в выбранном узле и текст во всех его дочерних элементах?
Дженна Квон
Это интересно и решает эту проблему, но что произойдет, когда ситуация станет более сложной? Есть более гибкий способ выполнить работу.
Энтони Ратледж
Без jQuery, document.querySelector (". Title"). ChildNodes [0] .nodeValue
Баладжи Гунасекаран,
57

Вы можете получить nodeValue первого дочернего узла, используя

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

http://jsfiddle.net/TU4FB/

Догберт
источник
4
Хотя это будет работать, это зависит от положения дочерних узлов. Если (когда) это изменится, он сломается.
Armstrongest
Если текстовый узел не является первым дочерним элементом, вы можете получить nullвозвращаемое значение.
Энтони Ратледж
15

Если вы имеете в виду получить значение первого текстового узла в элементе, этот код будет работать:

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

Вы можете увидеть это в действии здесь: http://jsfiddle.net/ZkjZJ/

Shadow Wizard - это ваше ухо
источник
Я думаю, вы могли бы использовать curNode.nodeType == 3вместо этого nodeName.
Nilloc
1
@Nilloc наверное, но в чем выгода?
Shadow Wizard is Ear For You
5
@ShadowWizard @Nilloc рекомендуемый способ для этого - использовать константы ... curNode.nodeType == Node.TEXT_NODE(числовое сравнение быстрее, но curNode.nodeType == 3 не читается - какой узел имеет номер 3?)
mikep
1
@ShadowWizard Использование curNode.NodeType === Node.TEXT_NODE. Это сравнение происходит в цикле неизвестных возможных итераций. Сравнение двух маленьких чисел лучше, чем сравнение строк разной длины (соображения времени и места). Правильный вопрос в этой ситуации - «какой тип / тип узла у меня есть?», А не «какое у меня имя?» developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
Энтони Ратледж
2
@ShadowWizard Также, если вы собираетесь использовать цикл для просеивания childNodes, знайте, что узел элемента может иметь более одного текстового узла. В общем решении может потребоваться указать, какой экземпляр текстового узла в узле элемента вы хотите настроить (первый, второй, третий и т. Д.).
Энтони Ратледж
14

Еще одно собственное JS-решение, которое может быть полезно для «сложных» или глубоко вложенных элементов, - это использование NodeIterator . Поместите NodeFilter.SHOW_TEXTв качестве второго аргумента («whatToShow») и перебирайте только дочерние текстовые узлы элемента.

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>

Вы также можете использовать TreeWalker. Разница между ними в том, что NodeIteratorэто простой линейный итератор, который также TreeWalkerпозволяет перемещаться через братьев и сестер и предков.

Юваль А.
источник
9

Чистый JavaScript: минимализм

Во-первых, всегда помните об этом при поиске текста в DOM.

MDN - Пробелы в DOM

Эта проблема заставит вас обратить внимание на структуру вашего XML / HTML.

В этом примере на чистом JavaScript я учитываю возможность наличия нескольких текстовых узлов, которые могут чередоваться с другими типами узлов. . Однако изначально я не осуждаю пробелы, оставляя эту задачу фильтрации другому коду.

В этой версии я прохожу NodeList передаю код вызова / клиента.

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

Конечно, если node.hasChildNodes()сначала провести тестирование , не нужно будет использовать forцикл предварительного тестирования .

/**
* 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: надежный

Здесь функция getTextById()использует две вспомогательные функции: getStringsFromChildren()и 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.
}

Затем возвращаемое значение (Array или null) отправляется клиентскому коду, где оно должно быть обработано. Будем надеяться, что массив должен содержать строковые элементы реального текста, а не строки пробелов.

Пустые строки ( "") не возвращаются, потому что вам нужен текстовый узел, чтобы правильно указать наличие допустимого текста. Returning ( "") может создать ложное впечатление о существовании текстового узла, что заставит кого-то предположить, что он может изменить текст, изменив значение .nodeValue. Это неверно, потому что текстовый узел не существует в случае пустой строки.

Пример 1 :

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

Пример 2 :

<p id="bio">

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

Проблема возникает, когда вы хотите упростить чтение HTML-кода за счет интервалов. Теперь, даже несмотря на отсутствие удобочитаемого допустимого текста, все еще есть текстовые узлы с символами новой строки ( "\n") в их .nodeValueсвойствах.

Люди рассматривают примеры один и два как функционально эквивалентные - пустые элементы, ожидающие заполнения. Модель DOM отличается от человеческого мышления. Вот почему getStringsFromChildren()функция должна определять, существуют ли текстовые узлы, и собирать .nodeValueзначения в массив.

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

Во втором примере два текстовых узла действительно существуют и getStringFromChildren()будут возвращать .nodeValueих обоих ( "\n"). Однако filterWhitespaceLines()использует регулярное выражение для фильтрации строк, состоящих из чисто пробельных символов.

Является ли возврат nullвместо "\n"символов новой строки ( ) формой лжи клиентскому / вызывающему коду? Говоря человеческим языком, нет. С точки зрения DOM, да. Однако проблема здесь в получении текста, а не в его редактировании. Нет человеческого текста для возврата к вызывающему коду.

Никогда нельзя узнать, сколько символов новой строки может появиться в чьем-либо HTML. Создание счетчика, который ищет «второй» символ новой строки, ненадежно. Возможно, этого не существует.

Конечно, в дальнейшем проблема редактирования текста в пустом <p></p>элементе с дополнительными пробелами (пример 2) может означать уничтожение (возможно, пропуск) всех текстовых узлов, кроме одного, между тегами абзаца, чтобы гарантировать, что элемент содержит именно то, что он есть предполагается отображать.

В любом случае, за исключением случаев, когда вы делаете что-то необычное, вам понадобится способ определить, какое .nodeValueсвойство текстового узла имеет истинный, читаемый человеком текст, который вы хотите отредактировать. filterWhitespaceLinesприводит нас на полпути.

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

На этом этапе вы можете получить следующий результат:

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

Нет гарантии, что эти две строки соседствуют друг с другом в DOM, поэтому их объединение .join()может привести к неестественной композиции. Вместо этого в коде, который вызываетgetTextById() вам нужно выбрать, с какой строкой вы хотите работать.

Протестируйте вывод.

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

Можно добавить .trim()внутрь, getStringsFromChildren()чтобы избавиться от начальных и конечных пробелов (или чтобы превратить кучу пробелов в строку нулевой длины ( ""), но как вы можете заранее знать, что каждое приложение, возможно, должно произойти с текстом (строкой) как только он будет найден? Нет, поэтому оставьте это для конкретной реализации и пусть getStringsFromChildren()будет общим.

Могут быть случаи, когда этот уровень специфичности ( targetи тому подобное) не требуется. Это отлично. В таких случаях используйте простое решение. Однако обобщенный алгоритм позволяет учесть простые и сложные ситуации.

Энтони Ратледж
источник
8

Версия ES6, возвращающая содержимое первого #text узла

const extract = (node) => {
  const text = [...node.childNodes].find(child => child.nodeType === Node.TEXT_NODE);
  return text && text.textContent.trim();
}
джуджуле
источник
Меня интересует эффективность и гибкость. (1) Использование .from()для создания экземпляра массива с неглубоким копированием. (2) Использование .find()для сравнения строк с использованием .nodeName. Использование node.NodeType === Node.TEXT_NODEбыло бы лучше. (3) Возврат пустой строки при отсутствии значения nullболее верен, если текстовый узел не найден. Если текстовый узел не найден, возможно, потребуется его создать! Если вы вернете пустую строку, ""вы можете создать ложное впечатление, что текстовый узел существует и с ним можно нормально работать. По сути, возвращение пустой строки - это белая ложь, и ее лучше избегать.
Энтони Ратледж
(4) Если в списке узлов более одного текстового узла, здесь нет способа указать, какой текстовый узел вы хотите. Вам может понадобиться первый текстовый узел, но вам может понадобиться последний текстовый узел.
Энтони Ратледж
Чем вы предлагаете заменить Array.from?
июль
@Snowman, пожалуйста, добавьте свой ответ на такие существенные изменения или дайте рекомендации OP, чтобы дать им возможность включить их в свой ответ.
TylerH
@jujule - Лучше использовать [...node.childNodes]для преобразования HTMLCollection в массивы
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
Праная Рана
источник
1
Я думаю, что метод стандартного javascript должен быть 'innerText'
Репортер
2
Это работает не так, как хочет OP - он также получит текст внутри aэлемента: jsfiddle.net/ekHJH
Джеймс Аллардис,
1
@James Allardice - Я закончил с jquery-решением, теперь это будет работать .................
Пранай Рана
Это почти сработает, но вам не хватает .в начале вашего селектора, что означает, что вы фактически получаете текст titleэлемента, а не элементы сclass="title"
Джеймс Аллардис
@reporter .innerText- это старая конвенция IE, принятая недавно. С точки зрения стандартного сценария DOM, node.nodeValueэто способ получения текста текстового узла.
Энтони Ратледж
2

Это также будет игнорировать пробелы, так что вы никогда не получили Blank textNodes..code с использованием основного 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;
    }
}

Проверьте это на jsfiddle: - http://jsfiddle.net/webx/ZhLep/

webx
источник
curNode.nodeType === Node.TEXT_NODEбыло бы лучше. Использование сравнения строк и регулярного выражения в цикле - решение с низкой производительностью, особенно при oDiv.childNodes.lengthувеличении величины . Этот алгоритм решает конкретный вопрос OP, но, возможно, с ужасной ценой производительности. Если расположение или количество текстовых узлов изменяется, то это решение не может гарантировать получение точного вывода. Другими словами, вы не можете настроить таргетинг на нужный текстовый узел. Вы находитесь во власти структуры HTML и расположения текста в нем.
Энтони Ратледж
1

Вы также можете использовать text()тест узлов XPath, чтобы получить только текстовые узлы. Например

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;
}
удвоить
источник
0

Это мое решение в ES6 для создания строки, запрещающей объединенный текст всех дочерних узлов (рекурсивный) . Обратите внимание, что это также посещение корня дочерних узлов.

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

Это решение было вдохновлено решением https://stackoverflow.com/a/41051238./1300775 .

Дэмиен
источник