JavaScript: Get textContent from element (with nested children) ignoring specific child - javascript

I'm hoping to get the textContent of an element (including text within nested children) but ignore a specific element which has text.
For example, such that
<div class="container">
<div class="exclude-text">Exclude me</div>
<div class="another-class">Add this text</div>
And this text here
</div>
Performing element.textContent returns Exclude me Add this text And this text here whereas I'd just like add this text And this text here.
My possible solution would be to iterate through childNodes while checking if classList.contains("exclude-text") but how would I still get the And this text here?
Thanks in advance!

You're on the right track looping through nodes. Do it recursively, building up the text of the text nodes, and recursing into elements if they don't match an exclude selector (which could be a group [".a, .b"], if you want to exclude multiple classes or similar):
function getTextExcept(element, exclude) {
return worker(element);
function worker(node, text = "") {
if (node.nodeType === Node.TEXT_NODE) {
text += node.nodeValue;
} else if (node.nodeType === Node.ELEMENT_NODE) {
if (!node.matches(exclude)) {
for (const child of node.childNodes) {
text = worker(child, text);
}
}
}
return text;
}
}
console.log(getTextExcept(
document.querySelector(".container"),
".exclude-text"
));
<div class="container">
<div class="exclude-text">Exclude me</div>
<div class="another-class">Add this text</div>
And this text here
</div>
An alternative approach is to clone the entire structure, remove the elements you don't want the text of, and then use .textContent on the result. For a really large structure, that might be problematic in terms of memory (but it would have to be really large).
function getTextExcept(element, exclude) {
const clone = element.cloneNode(true);
for (const child of clone.querySelectorAll(exclude)) {
child.remove();
}
return clone.textContent;
}
console.log(getTextExcept(
document.querySelector(".container"),
".exclude-text"
));
<div class="container">
<div class="exclude-text">Exclude me</div>
<div class="another-class">Add this text</div>
And this text here
</div>

Related

search for nested text in div

I can select a specific html element using
querySelector('.someClassName')
And then run tests to check aspects such as textContent.
I'd like to be able to select an element, like I did above, and then check if there is specific text in a nested html element. I'm thinking something like this:
expect(someContainer.querySelector('.someClassName').nestedTextContent)
.toEqual(expect.stringContaining('this is some nested text'))
//nestedTextContent not real: this is something I made up to show what I'd like to achieve
Is there something like this? Is my only option to iterate through the dom?
Use a recursive function to check the textContent of each element:
let res = null
function findTextContent(el, txt){
if(el && el.textContent.trim() === txt){
res = el
return res
}else{
if(el && el.hasChildNodes()){
[...el.children].forEach((ch) => {
return findTextContent(ch, txt)
})
}
return res
}
}
const el = findTextContent(document.querySelector(".grandparent"), "Child text")
el.style.color = "red"
<div class="grandparent">
Grandparent text
<div class="parent">
Parent text
<div class="child">Wrong child text</div>
</div>
<div class="parent">
Parent text
<div class="child">Wrong child text</div>
<div class="child">Child text</div>
<div class="child">Wrong child text</div>
</div>
</div>
You can use expect().stringContaining to check if a string has a certain substring (assuming you are using jest). To get the text contained in the div, use querySelector(....).innerText (again, assuming you are using jest and querySelector returns an HTMLElement.

How to select next sibling of a certain type with JS?

I've got some html
<h4 id="start-here">title</h4>
<p>paragraph</p>
<p>paragraph</p>
...some number of paragraphs...
link
And I've got the <h4> with the id selected in JavaScript. How do I get from that selection in JS to the first <a> which is of the class link, or just the next sibling anchor tag?
Using document.querySelector() and a CSS selector, here with the general sibling combinator ~, you can achieve that like this:
A side note, in below samples I target inline style, though it is in general better to toggle a class.
Stack snippet
(function(){
document.querySelector('#start-here ~ a.link').style.color = 'red';
})();
<h4 id="start-here">title</h4>
<p>paragraph</p>
link
<p>paragraph</p>
link
Updated based on another question/comment, how to get more than one element in return.
With document.querySelectorAll() one can do similar, and target multiple elements like this.
Stack snippet
(function(){
var elements = document.querySelectorAll('#div2, #div3');
for (var i = 0; i < elements.length; i++) {
elements[i].style.color = 'red';
}
})();
<h4 id="start-here1">title</h4>
<div id="div1">some text</div>
<h4 id="start-here2">title</h4>
<div id="div2">some text</div>
<h4 id="start-here3">title</h4>
<div id="div3">some text</div>
The "start-here" ID on your element makes this easy. But let's imagine you have a reference to a DOM element without such a convenient selector, and you don't want to add a temporary ID to it.
In that case, you could use XPath with document.evaluate and your DOM reference as the second argument. Let's say you have that reference in yourElement and you want the next <section> sibling
const nextSibling = document.evaluate("following-sibling::section", yourElement, null,
XPathResult.FIRST_ORDERED_NODE_TYPE).singleNodeValue
I think to start with the first sibling, then i put all the siblings inside an array. Hence I extract what you want.
var x = document.getElementById("stat-here");
console.log(x)
var result = [],
node = x.nextSibling;
while ( node ) {
if (node.nodeType === Node.ELEMENT_NODE ) {
result.push( node );
}
node = node.nextElementSibling || node.nextSibling;
}
console.log(result, '\n Result: ',result[result.length-2])
<h4 id="stat-here">title</h4>
<p>paragraph</p>
<p>paragraph</p>
link

Prevent .lastChild from returning a carriage return?

I'm trying to get the first and the last node of a given element.
document.getElementById('test').firstChild
document.getElementById('test').lastChild
http://codepen.io/FezVrasta/pen/gaPJXe
The problem is that the lastChild function return me just a carriage return instead of the last useful node.
What is the best way to return me the last element or text node excluding the carriage return?
lastChild and firstChild return the first and last child nodes of the element, that can be an element, a text node or a comment. If You want to get the first and last child elements you can use:
document.getElementById('test').firstElementChild
document.getElementById('test').lastElementChild
it should be supported in all modern browsers
Go through the previousSiblings until you find one that is an element node.
The nodeType == 1 means it's an element node... see documention for all types
var lastElement = document.getElementById('test').lastChild;
while (lastElement != null && lastElement.nodeType != 1) {
lastElement = lastElement.previousSibling;
}
if (lastElement != null) {
document.getElementById("output").innerHTML = lastElement.id;
}
<div id="test">
<div id="div1">a</div>
<div id="div2">b</div>
<div id="div3">c</div>
<div id="div4">d</div>
</div>
<div id="output"></div>

swap 2 div's index using pure javascript

I have a parent div and it has 9 same div's am trying to swap two div's index. Following is my code:
HTML:
<div id="cont" class="container">
<div class="no">1</div>
<div class="no">2</div>
<div class="no">3</div>
<div class="blank"></div>
<div class="no">4</div>
<div class="no">5</div>
<div class="no">6</div>
<div class="no">7</div>
<div class="no">8</div>
</div>
now I want to swap say 5th and 6th indexed elements. I have no clue how to do that in JavaScript. I know there is function called .index() but how to do that in pure JS.
Here's one implementation: http://jsfiddle.net/x8hWj/2/
function swap(idx1, idx2) {
var container = document.getElementById('cont');
// ditch text nodes and the like
var children = Array.prototype.filter.call(
container.childNodes,
function(node) {
return node.nodeType === 1;
}
);
// get references to the relevant children
var el1 = children[idx1];
var el2 = children[idx2];
var el2next = children[idx2 + 1];
// put the second element before the first
container.insertBefore(el2, el1);
// now put the first element where the second used to be
if (el2next) container.insertBefore(el1, el2next);
else container.appendChild(el1);
}
This starts by getting a list of all element child nodes, then uses insertBefore to rearrange them.

How can I check if an element (h2) is really the first element inside a div?

I want to "manage" the first h2 element inside a div, only if it's really the "first element"
<div id="test">
<h2>Try 1</h2>
Test Test Test
<h2>Try 2</h2>
</div>
here only h2 with text "Try 1" must be managed
<div id="test">
Test Test Test
<h2>Try 1</h2>
<h2>Try 2</h2>
</div>
Here no (there is text before).
How can I do it with jQuery?
No jquery needed for that, just take:
document.getElementById('test').firstChild.nodeName // gives the name of the node
This will give you the name of the very first node, even if it's not a tag but just a plain text-node!
optionally you could of course use document.querySelector() if you want to be more flexible with your selectors and know that most of the clients browser support it.
To be clear: if you add a newline, this will also be considered as a text-node, so the heading needs to start on the same line or you will get #text as result for both examples!
This will fail:
<div id="test">
<h2>Try 1</h2>
Test Test Test
<h2>Try 2</h2>
</div>
and this will work:
<div id="test"><h2>Try 1</h2>
Test Test Test
<h2>Try 2</h2>
</div>
a little demo for you
This is how you would filter to get only the header that is the first node, ignoring all blank text nodes:-
$("#test").children("h2").first().filter(function() {
var childNodes = this.parentNode.childNodes;
var i = 0;
var textNode = 3;
// No children
if(!childNodes.length) {
return false;
}
// Skip blank text node
if(childNodes[i].nodeType === textNode && childNodes[i].textContent.trim().length === 0) {
i ++;
}
// Check we have a match
return childNodes[i] === this;
});
Here is it in action http://jsfiddle.net/nmeXw/
I think I found myself a solution, a bit funny :
if($.trim($('#test').html()).substr(0,4).toLowerCase() == "<h2>")
{
$('#test h2:first').css('background-color', 'red');
}
What do you think about? :)
You can use .contents to conditionally ignore leading nodes that are only whitespace text. Then see if the first node is an <h2> (Fiddle):
function isFirstChildH2(selector) {
// get th efirst node, which may be a text node
var firstNode = $(selector).contents().first();
// if the first node is all whitespace text, ignore it and go to the next
if(firstNode[0].nodeType == 3 && firstNode.text().match(/\S/g) == null) {
firstNode = firstNode.next();
}
if(firstNode.is("h2")) {
// it's an h2; do your magic!
alert("h2 is the first thing on " + selector)
} else {
// first node is either non-whitespace text or an non-h2 element
// don't do your magic
alert("h2 is NOT the first thing on " + selector)
}
}
isFirstElementH2("#test");
The challenge we're facing here is that javascript recognizes whitespace as a text node as well. Therefore, from a javascript point of view, this HTML:
<div id="test">
<h2>Try 1</h2>
Test Test Test
<h2>Try 2</h2>
</div>
Is different from this HTML:
<div id="test"><h2>Try 1</h2>
Test Test Test
<h2>Try 2</h2>
</div>
In the first case, the first node inside the div is a textNode (nodeType == 3)
In the second HTML example, the first node inside the div is a h2 node.
I've come up with a solution for this, a handy function that loops through all elements combining jQuery and native javascript.
Solution
var objNodes = $(".wrapper").contents().get();
function loopNodes(objNodes, i) {
i = (typeof i === "undefined") ? 0 : i;
if (objNodes[i].nodeType !== 3) {
return {"isHeader":true, "first":$(objNodes[i])};
} else {
var strText = objNodes[i].innerText || objNodes[i].textContent;
if ($.trim(strText).length === 0) {
return loopNodes(objNodes, i+1);
} else {
return {"isHeader":false, "first":null};
}
}
}
Usage
var objResults = loopNodes(objNodes);
if (objResults.isHeader) {
console.log("Coolness");
objResults.first.text("AWESOME FIRST HEADER!");
} else {
console.log("Less Coolness");
}
In action:
http://jsbin.com/welcome/61883/edit
Edit: Added the cross-browser way of getting innerText/textContent. See Quirksmode for full reference on the matter.
OK, let's mix some jQuery with plain DOM code (as jQuery is not capable of handling text nodes):
var $el = $("#test > h2:first-child");
if (!$el.length) return false;
var el = $el.get(0),
reg = /\S/; // no whitespace
for (var prev = el; prev = prev.previousSibling; )
if (prev.nodeType == 3 && reg.test(prev.data))
return false;
return el;
Demo at jsfiddle.net

Categories