How to search for value in all descendant textNodes(Javascript)? - javascript

So basically I am making a Chrome extension which sorts the search results on Ebay by their popularity(number of times sold). To do this I need to find all li elements which have a descendant text node containing the text ...+ Sold where ... is a number.
Basically, search results on Ebay look like this:
<li class="s-item">
<div class="s-item__wrapper clearfix">
<div class="s-item__image-section">
<!-- other stuff -->
</div>
<div class="s-item__info clearfix">
<!-- other stuff -->
<div class="s-item__details clearfix">
<!-- other stuff -->
<div><span><span>62+ Sold</span></span></div>
</div>
</div>
</div>
</li>
In every li element I have to search for the text Sold and extract the number out of that text node to process it further. How can I do that?

You cannot do that only by using childNodes or children properties because they return only the children and not all descendants of the current node. So you will have to write your own function for that, something like:
function getDescendants(node, arr) {
var i;
arr = arr || [];
for (i = 0; i < node.childNodes.length; i++) {
arr.push(node.childNodes[i])
getDescendants(node.childNodes[i], arr);
}
return arr;
}
Using this function, you just simply iterate over all descendants and check if they are text nodes ( nodeType == 3 ) and after that search for the word Sold in them. Extracting the number is pretty easy after that.
Something like:
var searchValue = "Sold";
var descendants = getDescendants(listItem);
for(var j = 0; j < descendants.length; j++) {
if(descendants[j].nodeType == 3){
if(descendants[j].nodeValue.indexOf(searchValue) > -1){
var text = descendants[j].nodeValue.trim();
//"37+ Sold" for example
var soldNr = text.substring(0, text.indexOf(searchValue)-2);
//you process your number(soldNr) further
}
}
}

Use a selector string: select li.s-item span to select all spans which descend from an li with a class of s-item, check to see if the span's only child is a text node with "Sold" in it, and if so, do whatever you need to do with it.
If you're sure that any <li> will do, and not just those with a class of s-item, then use 'li span' instead:
document.querySelectorAll('li span').forEach(({ childNodes, textContent }) => {
if (childNodes.length !== 1 || childNodes[0].nodeType !== 3 || !textContent.includes('Sold')) return;
const count = textContent.match(/\d+/);
console.log('Processing span with sold number ' + count);
});
<ul>
<li class="s-item">
<div class="s-item__wrapper clearfix">
<div class="s-item__image-section">
<!-- other stuff -->
</div>
<div class="s-item__info clearfix">
<!-- other stuff -->
<div class="s-item__details clearfix">
<!-- other stuff -->
<div><span><span>62+ Sold</span></span>
</div>
</div>
</div>
</div>
</li>
<li class="s-item">
<div class="s-item__wrapper clearfix">
<div class="s-item__image-section">
<!-- other stuff -->
</div>
<div class="s-item__info clearfix">
<!-- other stuff -->
<div class="s-item__details clearfix">
<!-- other stuff -->
<div><span><span>333+ Sold</span></span>
</div>
</div>
</div>
</div>
</li>
</ul>

Related

Trying to add a class for clicked elements but if statement doesn't work

<div>
<div class=category> Birthdays </div>
<div class=category> Anniversaries </div>
<div class=category> Newborns </div>
<div class=category> Weddings </div>
</div>
<ul>
<li class="product-filter-items">Birthdays</li>
<li class="product-filter-items">Weddings</li>
<li class="product-filter-items">Newborns</li>
<li class="product-filter-items">Anniversaries</li>
</ul>
<script>
let proCatList = document.querySelectorAll(".category")
let proFilterItems = document.querySelectorAll(".product-filter-items")
for(let i = 0; i < proFilterItems.length; i++){
proFilterItems[i].addEventListener("click" , function(){
if (proCatList[i].textContent.toUpperCase().replace(/[\n\r]+|[\s]{2,}/g, ' ').trim() == proFilterItems[i].textContent.toUpperCase().replace(/[\n\r]+|[\s]{2,}/g, ' ').trim() ){
proFilterItems[i].classList.add("active-filter")
console.log("Class Added")
}
})
}
</script>
I am trying to add a class based on a click event. What I am trying to do is if classname, category and product-filter-items are equal then it should add a classname called active-filter on click. Can anyone point out why this statement does not recognise the textContent of proCatList?
The iteration param i can't be used in the event handler. Therefor you could use the index of the clicked element, which you can call with this.
For getting the index, you can use the following small function:
const getIndex = elem => [...elem.parentNode.children].indexOf(elem);
Working example:
let proCatList = document.querySelectorAll(".category");
let proFilterItems = document.querySelectorAll(".product-filter-items");
/* small function for getting the index */
const getIndex = elem => [...elem.parentNode.children].indexOf(elem);
for (let i = 0; i < proFilterItems.length; i++) {
proFilterItems[i].addEventListener("click", function() {
const elem_index = getIndex(this);
if (proCatList[elem_index].textContent.toUpperCase().replace(/[\n\r]+|[\s]{2,}/g, ' ').trim() == this.textContent.toUpperCase().replace(/[\n\r]+|[\s]{2,}/g, ' ').trim()) {
this.classList.add("active-filter");
console.log("Class Added");
}
})
}
<div>
<div class=category> Birthdays </div>
<div class=category> Anniversaries </div>
<div class=category> Newborns </div>
<div class=category> Weddings </div>
</div>
<ul>
<li class="product-filter-items">Birthdays</li>
<li class="product-filter-items">Weddings</li>
<li class="product-filter-items">Newborns</li>
<li class="product-filter-items">Anniversaries</li>
</ul>

Javascript reorder HTML element based on attribute value

I have the following HTML:
<div class="wrapper">
<div class="item" id="item-1">
</div>
<div class="item" id="item-2">
</div>
<div class="item" id="item-3">
</div>
</div>
And in javascript I'm currently applying filters & sort to the results of an array:
results = Object.keys(list).filter(.....);
results = results.sort((a, b) => (....) ? 1 : -1);
// After the results have been filtered & sorted, hide all HTML elements:
document.querySelectorAll('.item').forEach(i => i.classList.add('d-none'));
// And then proceed to show only the results that have been filtered & sorted:
results.forEach(index =>
{
let id = list[index].id;
let item = document.getElementById('item-' + id);
item.classList.remove('d-none');
});
This works great. The problem is that now I need to move the HTML elements according to the results array, specifically with the id field.
A) Expected output: Array ids [2, 1]
<div class="wrapper">
<div class="item" id="item-2">
</div>
<div class="item" id="item-1">
</div>
<div class="item d-none" id="item-3">
</div>
</div>
B) Expected output: Array ids [2]
<div class="wrapper">
<div class="item" id="item-2">
</div>
<div class="item d-none" id="item-1">
</div>
<div class="item d-none" id="item-3">
</div>
</div>
You can move elements to the top, using prepend function.
On bellow example, I only implement the logic about move elements, and I didn't protected against non existent elements.
You should add your logic's about filtering, etc. and protect for possible errors.
function sortDivs() {
let list = [2, 1];
let mainDiv = document.querySelectorAll('.wrapper')[0];
list.reverse().forEach(n => mainDiv.prepend(document.getElementById('item-'+n)));
}
.item {
border: black solid;
}
<div class="wrapper">
<div class="item" id="item-1">
item-1
</div>
<div class="item" id="item-2">
item-2
</div>
<div class="item" id="item-3">
item-3
</div>
</div>
<br>
<button onClick="sortDivs()"> sort DIVs</button>
One way would be to
iterate the result ids and get the correspondig object from the dom (combined with your last .forEach round removing the d-done class)
get all d-done elements and combine both lists
convert to html and reset innerHTML of the wrapper
let results = [2,1]
let wrapper = document.getElementById('wrapper-id')
wrapper.innerHTML = results.map(id => document.querySelector('#item-' + id))
.concat([...document.querySelectorAll('.d-none')])
.map(elem => elem.outerHTML)
.join('')
<div class="wrapper" id="wrapper-id">
<div class="item" id="item-1">
item-1
</div>
<div class="item" id="item-2">
item-2
</div>
<div class="item d-none" id="item-3">
item-3
</div>
</div>
Solved.
I had several problems:
The first one was instead of using Object.keys(results).filter I should be using results.filter, because I don't need to get only the Keys, which was making things way harder.
Secondly, the logic to apply in order to have everything re-organizing according to multiple filters is:
Sort / filter everything
Hide all items (using d-none)
Grab all the wrapper children const wrapperItems = wrapper.children
Create a variable named wrapperNewItems that holds the new sorted/filtered items
Create a variable that holds which ID's (item-1, item-2, etc) have been sorted/filtered
Push the items sorted into the variable and remove d-none
Push the items that were NOT sorted into the variable and keep d-none
Translated into code:
document.querySelectorAll('.item').forEach(i => i.classList.add('d-none'));
const wrapper = document.getElementsByClassName('wrapper')[0];
// Saves the existing children
const wrapperItems = wrapper.children;
// Holds the new items ordered perfectly
let wrapperNewItems = [];
// Holds names that were filtered (item-1, item-2, etc)
let listOfNamesFiltered = [];
// Shows only the items filtered
results.forEach(item =>
{
let name = 'item-' + item.id;
let el = document.getElementById(name);
el.classList.remove('d-none');
wrapperNewItems.push(el);
listOfNamesFiltered.push(name);
});
for (let i = 0; i < wrapperItems.length; i++)
{
let item = wrapperItems[i];
let name = item.id; // id="" already contains "item-{id}"
// If the name already exists in the list, we won't add it again
if (listOfNamesFiltered.includes(name))
continue;
wrapperNewItems.push(item);
}
// Clears the div
wrapper.innerHTML = '';
// Appends the items once again
for (let i = 0; i < wrapperNewItems.length; i++)
wrapper.innerHTML += wrapperNewItems[i].outerHTML;

How to get children of a div

User can, by pressing a button, select a particular topic of interest. When that happens, various divs will either become visible or invisible depending on whether that div has a link referring to that topic.
function GetPostsByTopic(topic) {
var area = document.getElementById("postArea");
var topicAreas = area.getElementsByClassName("topicArea");
for (i = 0; i < topicAreas.length; i++) {
var children = topicAreas[i].children;
var topics = [];
for (j = 0; j < children.length; j++) {
topics.push(children[j].getAttribute("asp-route-name"));
document.getElementById("firstTest").innerHTML = children[j].toString();
}
var b = topics.includes(topic);
if (b == true) {
var parentId = document.getElementById(topicAreas[i]).parentNode.id;
document.getElementById(parent).style.display = 'block';
} else {
document.getElementById(parent).style.display = 'none';
}
}
}
<div class="topicBox">
<button class="topicButton" onclick="GetPostsByTopic('Pets')">Pets</button>
<button class="topicButton" onclick="GetPostsByTopic('Vacation')">Vacation</button>
</div>
<div id="postArea">
<div class="post" id="post1">
<div class="topicArea">
<a asp-action="Topic" asp-route-name="Pets">Pets</a>
</div>
</div>
<div class="post" id="post2">
<div class="topicArea">
<a asp-action="Topic" asp-route-name="Vacation">Vacation</a>
</div>
</div>
<div class="post" id="post3">
<div class="topicArea">
<a asp-action="Topic" asp-route-name="Pets">Pets</a>
</div>
</div>
</div>
The trouble, as far as I can tell, begin early in the JS part. I can see that when a do var children=topicAreas[i].children, I get nothing.
I hope this is what you're trying to do. Based on what button you click, respective div is displayed.
function GetPostsByTopic(topic) {
var area = document.getElementById("postArea");
var topicAreas = area.getElementsByClassName("topicArea");
for (i = 0; i < topicAreas.length; i++) {
var children = topicAreas[i].children;
for (j = 0; j < children.length; j++) {
var parentId = topicAreas[i].parentNode.id;
if(children[j].getAttribute("asp-route-name") === topic){
document.getElementById(parentId).style.display = 'block';
}else{
document.getElementById(parentId).style.display = 'none';
}
}
}
}
<div class="topicBox">
<button class="topicButton" onclick="GetPostsByTopic('Pets')">Pets</button>
<button class="topicButton" onclick="GetPostsByTopic('Vacation')">Vacation</button>
</div>
<div id="postArea">
<div class="post" id="post1">
<div class="topicArea">
<a asp-action="Topic" asp-route-name="Pets">Pets</a>
</div>
</div>
<div class="post" id="post2">
<div class="topicArea">
<a asp-action="Topic" asp-route-name="Vacation">Vacation</a>
</div>
</div>
<div class="post" id="post3">
<div class="topicArea">
<a asp-action="Topic" asp-route-name="Pets">Pets</a>
</div>
</div>
</div>
Children isn't the issue. When you run your code you get the error "Uncaught TypeError: Cannot set property 'innerHTML' of null". Looking at your code where you are using .innerHTML, we see that you are trying to reference an element that you don't have in this code:
document.getElementById("firstTest")
Now, after adding that, you still have some items that you should change.
asp-action and asp-route-name are invalid HTML. Are you using a
framework that requires this syntax?
Don't use .getElementsByClassName().
Use .querySelectorAll() and Array.forEach() on the result for
easier looping.
Don't use .innerHTML when you aren't working with HTML strings as there are security and performance implications to doing so.
Avoid inline styles when you can. Using them causes duplication of code and code is harder to scale. Instead, use CSS classes and the .classList API.
It's not super clear exactly what is supposed to happen when clicking your buttons, but see the updated code below:
function GetPostsByTopic(topic) {
var area = document.getElementById("postArea");
// Don't use .getElementsByClassName() as it provides a live node list
// and causes quite a performance hit, especially when used in loops.
// Use .querySelectorAll() and then use .forEach() on the collection that
// it returns to iterate over them.
area.querySelectorAll(".topicArea").forEach(function(area){
var topics = [];
// No need for children, here. Again, use .querySelectorAll()
area.querySelectorAll("*").forEach(function(child) {
topics.push(child.getAttribute("asp-route-name"));
document.getElementById("firstTest").textContent = child.getAttribute("asp-route-name");
});
if (topics.indexOf(topic) > -1) {
// Don't use inline styles if you can avoid it.
// Instead use pre-made classes.
area.classList.add("hidden");
}
else {
area.classList.remove("hidden");
}
});
}
/* Use CSS classes when possible instead of inline styles */
.hidden { display:none; }
<div class="topicBox">
<button class="topicButton" onclick="GetPostsByTopic('Pets')">Pets</button>
<button class="topicButton" onclick="GetPostsByTopic('Vacation')">Vacation</button>
</div>
<div id="postArea">
<div class="post" id="post1">
<div class="topicArea">
<a asp-action="Topic" asp-route-name="Pets">Pets</a>
</div>
</div>
<div class="post" id="post2">
<div class="topicArea">
<a asp-action="Topic" asp-route-name="Vacation">Vacation</a>
</div>
</div>
<div class="post" id="post3">
<div class="topicArea">
<a asp-action="Topic" asp-route-name="Pets">Pets</a>
</div>
</div>
</div>
<div id="firstTest"></div>

JQuery - Find element that has a given number of siblings

I need to be able to search through each level of descendants of a JQuery object, and return any collections of siblings that may have a given number of members at that hierarchy level, starting with the objects most distant descendants, and working its way back up.
So given some HTML:
<div class="search-me">
<div> <!-- DON'T FIND THIS -->
<p>FIND ME</p>
<p>FIND ME</p>
<p>FIND ME</p>
</div>
<div> <!-- DON'T FIND THIS -->
<span></span>
<span></span>
</div>
<div> <!-- DON'T FIND THIS -->
<div>FIND ME</div>
<div>FIND ME</div>
<div>FIND ME</div>
</div>
</div>
I need the following pseudo code, to return the items labelled 'FIND ME', and nothing else...
$('.search-me').findGroupWithSize(3);
Also, note that there are three div's containing the <p>, <span>, and <div> tags, which should not be returned. So it should return a collection of the lowest level set of elements, whose siblings total a given amount.
The below example finds the groups of 3 from the HTML you posted.
var siblingsGroupTarget = 3;
var foundIndex = 0;
var elementArray = [];
$(".search-me").find("*").each(function(index){
// Conditions
var hasChilds = $(this).children().length;
var hasSiblings = $(this).siblings().length;
var itsSiblings = $(this).siblings();
var itsSiblingsHasChilds = itsSiblings.children().length;
console.log( $(this)[0].tagName+": "+$(this).siblings().length );
if( hasChilds == 0 && ( hasSiblings == siblingsGroupTarget-1 && itsSiblingsHasChilds == 0 ) ){
elementArray.push( $(this) );
}
});
elementArray.reverse();
for(i=0;i<elementArray.length;i++){
foundIndex++;
elementArray[i].addClass("FOUND").append(" I was found #"+foundIndex);
};
.FOUND{
border:3px solid red;
width:12em;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="search-me">
<div> <!-- DON'T FIND THIS -->
<p>FIND ME</p>
<p>FIND ME</p>
<p>FIND ME</p>
</div>
<div> <!-- DON'T FIND THIS -->
<span></span>
<span></span>
</div>
<div> <!-- DON'T FIND THIS -->
<div>FIND ME</div>
<div>FIND ME</div>
<div>FIND ME
<ul> <!-- DON'T FIND THIS -->
<li>FIND ME</li>
<li>FIND ME</li>
<li>FIND ME</li>
</ul>
</div>
</div>
</div>
You can use selector ".search-me > *" passed to jQuery() to get direct descendants of .search-me element, .toArray() to convert jQuery object to array, Array.prototype.reverse() to reverse the collection of elements, .filter() to check if the child elements .length is N, wrap array methods in jQuery() with selector "> *" to return child elements of matched elements
const N = 3;
let res = $("> *", $(".search-me > *").toArray().reverse()
.filter(function(el) {return $("> *", el).length === N}));
console.log(res);
res.css("color", "purple")
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js">
</script>
<div class="search-me">
<div> <!-- DON'T FIND THIS -->
<p>FIND ME</p>
<p>FIND ME</p>
<p>FIND ME</p>
</div>
<div> <!-- DON'T FIND THIS -->
<span>DON'T FIND ME</span>
<span>DON'T FIND ME</span>
</div>
<div> <!-- DON'T FIND THIS -->
<div>FIND ME</div>
<div>FIND ME</div>
<div>FIND ME</div>
</div>
</div>

How to check if the content of the nested tags exists or not

Suppose I have a structure like this in my div
<div class="mainDiv">
<div class="addrLabel">Address</div>
<div class="addrValue">
<!-- this content value i want to check if exist, then grab it else no value -->
the content which i want to check if exist or not. if a single value is there i want to grab it also
<span class="hrefClass">
next link
</span>
</div>
<div class="clearfix"></div>
Second time I am repeating this div with null value
<div class="mainDiv">
<div class="addrLabel">Address</div>
<div class="addrValue">
<!-- no value at all -->
<span class="hrefClass">
next link
</span>
</div>
<div class="clearfix"></div>
How can I achieve this
try this demo... it checks if the nodeType of the contents() div.addrValue is 3 or a text node
var addrArray = [];
$(".addrValue").each(function(ndx, elem) {
var text = $(elem).contents()
.filter(function() {
if (this.nodeType === 3) { // chech for text node
if ($(this).text().trim().length !== 0) {
return true;
}
}
}).text();
if (text.length === 0) { // check string length
addrArray.push('no value at all');
} else {
addrArray.push(text.trim());
}
});
console.log(addrArray)
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="mainDiv">
<div class="addrLabel">Address</div>
<div class="addrValue">
<!-- this content value i want to check if exist, then grab it else no value -->
the content which i want to check if exist or not. if a single value is there i want to grab it also
<span class="hrefClass">
next link
</span>
</div>
<div class="clearfix"></div>
</div>
<div class="mainDiv">
<div class="addrLabel">Address</div>
<div class="addrValue">
<span class="hrefClass">
next link
</span>
</div>
<div class="clearfix"></div>
</div>
You can check if .text() equals false. Like this:
var addrArray = [];
if($('.addrValue').text() != false)
{
addrArray.push($(this).text());
}else{
addrArray.push('no value present');
}
Check the Fiddle: https://jsfiddle.net/ur5fntvp/
EDIT
You can also use .html() method instead of .text(). It depends on whether you want to get all HTML tags or text only
$( document ).ready(function() {
var a = $('.addrValue').text()
if(a.length) {
console.log('exists')
} else {
console.log("doesn't exists")
}
})
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="mainDiv">
<div class="addrLabel">Address</div>
<div class="addrValue">
<!-- this content value i want to check if exist, then grab it else no value -->
the content which i want to check if exist or not. if a single value is there i want to grab it also
<span class="hrefClass">
next link
</span>
</div>
<div class="clearfix"></div>

Categories

Resources