Using jQuery to gather all text nodes from a wrapped set, separated by spaces
I'm looking for a way to gather all of the text in a jQuery wrapped set, but I need to create spaces between sibling nodes that have no text nodes between them.
For example, consider this HTML:
<div>
<ul>
<li>List item #1.</li><li>List item #2.</li><li>List item #3.</li>
</ul>
</div>
If I simply use jQuery's text()
method to gather the text content of the <div>
, like such:
var $div = $('div'), text = $div.text().trim();
alert(text);
that produces the following text:
List item #1.List item #2.List item #3.
because there is no whitespace between each <li>
element. What I'm actually looking for is this (note the single space between each sentence):
List item #1. List item #3. List item #3.
This suggest to me that I need to traverse the DOM nodes in the wrapped set, appending the text for each to a string, followed by a space. I tried the following code:
var $div = $('div'), text = '';
$div.find('*').each(function() {
text += $(this).text().trim() + ' ';
});
alert(text);
but this produced the following text:
This is list item #1.This is list item #2.This is list item #3. This is list item #1. This is list item #2. This is list item #3.
I assume this is because I'm iterating through every descendant of <div>
and appending the text, so I'm getting the text nodes within both 开发者_JAVA技巧<ul>
and each of its <li>
children, leading to duplicated text.
I think I could probably find/write a plain JavaScript function to recursively walk the DOM of the wrapped set, gathering and appending text nodes - but is there a simpler way to do this using jQuery? Cross-browser consistency is very important.
Thanks for any help!
jQuery deals mostly with elements, its text-node powers are relatively weak. You can get a list of all children with contents()
, but you'd still have to walk it checking types, so that's really no different from just using plain DOM childNodes
. There is no method to recursively get text nodes so you would have to write something yourself, eg. something like:
function collectTextNodes(element, texts) {
for (var child= element.firstChild; child!==null; child= child.nextSibling) {
if (child.nodeType===3)
texts.push(child);
else if (child.nodeType===1)
collectTextNodes(child, texts);
}
}
function getTextWithSpaces(element) {
var texts= [];
collectTextNodes(element, texts);
for (var i= texts.length; i-->0;)
texts[i]= texts[i].data;
return texts.join(' ');
}
This is the simplest solution I could think of:
$("body").find("*").contents().filter(function(){return this.nodeType!==1;});
You can use the jQuery contents() method to get all nodes (including text nodes), then filter down your set to only the text nodes.
$("body").find("*").contents().filter(function(){return this.nodeType!==1;});
From there you can create whatever structure you need.
I built on @bobince's terrific answer to make search tool that would search all columns of a table and filter the rows to show only those that matched (case-insensitively) all of a user's search terms (provided in any order).
Here is a screenshot example:
And here is my javascript/jQuery code:
$(function orderFilter() {
// recursively collect all text from child elements (returns void)
function collectTextNodes(element, texts) {
for (
let child = element.firstChild;
child !== null;
child = child.nextSibling
) {
if (child.nodeType === Node.TEXT_NODE) {
texts.push(child);
} else if (child.nodeType === Node.ELEMENT_NODE) {
collectTextNodes(child, texts);
}
}
}
// separate all text from all children with single space
function getAllText(element) {
const texts = [];
collectTextNodes(element, texts);
for (let i = texts.length; i-- > 0; ) texts[i] = texts[i].data;
return texts.join(' ').replace(/\s\s+/g, ' ');
}
// check to see if the search value appears anywhere in child text nodes
function textMatchesFilter(tbody, searchVal) {
const tbodyText = getAllText(tbody).toLowerCase();
const terms = searchVal.toLowerCase().replace(/\s\s+/g, ' ').split(' ');
return terms.every(searchTerm => tbodyText.includes(searchTerm));
}
// filter orders to only show those matching certain fields
$(document).on('keyup search', 'input.js-filter-orders', evt => {
const searchVal = $(evt.target).val();
const $ordersTable = $('table.js-filterable-table');
$ordersTable.find('tbody[hidden]').removeAttr('hidden');
if (searchVal.length <= 1) return;
// Auto-click the "Show more orders" button and reveal any collapsed rows
$ordersTable
.find('tfoot a.show-hide-link.collapsed, tbody.rotate-chevron.collapsed')
.each((_idx, clickToShowMore) => {
clickToShowMore.click();
});
// Set all tbodies to be hidden, then unhide those that match
$ordersTable
.find('tbody')
.attr('hidden', '')
.filter((_idx, tbody) => textMatchesFilter(tbody, searchVal))
.removeAttr('hidden');
});
});
For our purposes, it works perfectly! Hope this helps others!
精彩评论