Is it possible to select the deepest DOM child in cheerio? - javascript

Is there a way to select the deepest child in each branch (specifically divs) in cheerio?
Example:
<div id="parent">
<div>
<div id="dontSelectThisSinceThereIsADeeperDiv"></div>
<div>
<div id="selectThis"></div>
</div>
</div>
<div id="selectThisAlso"></div>
<div>
<div id="selectThisAsWell"></div>
</div>
</div>
Basically, all the divs that I want to select are the deepest within their "branch" from the parent. Is there a way to possibly do this in cheerio?

It doesn't look like there is a single function to do what you require. But you can create your own function by utilising different cheerio functions. For a recursive example (not tested, but hopefully you get the idea):
function getLeaves(parent, result = []) {
let children = $(parent).children()
if(children.length > 0){
children.each((i, elem) => getLeaves(elem, result))
}else{
result.push(parent)
}
return result
}
let leaves = getLeaves('#parent')

Related

How to re-Order html child elements in Dom based on id value

I have a parent div with some child elements. I want to re-order child elements based on two id values. for example 1,4. It means to grab the item with id 1 and insert it above the item with id 4.
<div class="parent">
<div id="1">First</div>
<div id="2">Second</div>
<div id="3">Third</div>
<div id="4">Fourth</div>
<div id="5">Fifth</div>
</div>
Making a drag and drop component for react. And this is what i have tried
const element = document.getElementById('1') //dragStart
const targetElement = document.getElementById('4') //dragEnter
const parent = document.querySelector('.parent') // drop
parent.insertBefore(element, targetElement)
But problem is when i grab the first element and want to put it on the bottom (last child). It fails to do so. How to put a child element after last child with insertBefore() method?
Don't know how you are using insertBefore() but there should not be any issues:
Update: The issue could be that your code is running before the DOM is fully loaded. You can wrap your code with DOMContentLoaded:
<script>
document.addEventListener('DOMContentLoaded', (event) => {
const element = document.getElementById('1') //dragStart
const targetElement = document.getElementById('4') //dragEnter
const parent = document.querySelector('.parent') // drop
parent.insertBefore(element, targetElement)
});
</script>
<div class="parent">
<div id="1">First</div>
<div id="2">Second</div>
<div id="3">Third</div>
<div id="4">Fourth</div>
<div id="5">Fifth</div>
</div>
Placing the first element as the last element using nextSibling:
<script>
document.addEventListener('DOMContentLoaded', (event) => {
const parentNode = document.querySelector('.parent');
const element = document.getElementById('1') //dragStart
const targetElement = document.querySelector('.parent').lastElementChild //get last child
parentNode.insertBefore(element, targetElement.nextSibling);
});
</script>
<div class="parent">
<div id="1">First</div>
<div id="2">Second</div>
<div id="3">Third</div>
<div id="4">Fourth</div>
<div id="5">Fifth</div>
</div>
Note: This answers the original question. The question has now been edited to reference React. You wouldn't use the following in a React project. You'd reorder the state that the DOM represents, and then let React handle updating the DOM.
You're right to use insertBefore:
function moveElement(move, before) {
// Get the element to move
const elToMove = document.getElementById(move);
// Get the element to put it in front of
const elBefore = document.getElementById(before);
// Move it
elBefore.parentNode.insertBefore(elToMove, elBefore);
}
function moveElement(move, before) {
const elToMove = document.getElementById(move);
const elBefore = document.getElementById(before);
elBefore.parentNode.insertBefore(elToMove, elBefore);
}
setTimeout(() => {
moveElement("1", "4");
}, 800);
<div class="parent">
<div id="1">First</div>
<div id="2">Second</div>
<div id="3">Third</div>
<div id="4">Fourth</div>
<div id="5">Fifth</div>
</div>
Side note: I suggest avoiding having id values that start with digits. Although they're perfectly valid HTML and they work just fine with getElementById, they're a pain if you need to target them with CSS, because a CSS ID selector (#example) can't start with an unescaped digit. For instance, document.querySelector("#1") fails. You have to escape the 1 with a hex sequence, which isn't terrifically clear: document.querySelector("#\\31") (the characters \, 3, and 1: 0x31 = 49 = the Unicode code point for 1).

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.

Find distance between elements in the DOM

There is a root element in the DOM tree and there is another element inside this root element nested somewhere. How do I calculate how nested is this another element inside the root element?
What I would like to know is essentially how many times I have to get the parent element of the nested element until I get to the root element. So I can solve this problem by iterating on the parents until I get to the root element, like in this fiddle.
const first = document.getElementById('search-target-1');
let parent = first.parentElement;
let level = 0;
do {
parent = parent.parentElement;
level++;
}
while (!parent.classList.contains('root'))
console.log(`The first element is ${level} levels deep inside the root div.`);
const second = document.getElementById('search-target-2');
parent = second.parentElement;
level = 0;
do {
parent = parent.parentElement;
level++;
}
while (!parent.classList.contains('root'));
console.log(`The second element is ${level} level deep inside the root div.`);
<div class="root">
<div class="first-level">
<div class="second-level" id="search-target-1">
<!-- How deep is this element? -->
</div>
</div>
<div class="first-level"></div>
<div class="first-level">
<div class="second-level">
<div class="third-level" id="search-target-2">
<!-- And this one? -->
</div>
</div>
</div>
</div>
Is there a better way of achieving this? I am looking for a javascript api to get the same result.
The element.matchesSelector does not solve my problem, as I know the target element is inside the root element, but I don't know how deep it is.
You could use jQuery's .parentsUntil() function to accomplish this:
var searchDistance_1 = $("#search-target-1").parentsUntil(".root").length; // 1
var searchDistance_2 = $("#search-target-2").parentsUntil(".root").length; // 2
That gives you the number of parents in between the child and root you are looking for. If you're looking for the number of jumps up the hierarchy needed to get to the parent, you can just add 1.
If you need to do this in vanilla JS, you could look through the source code for this function on GitHub.
Your code works, but as Niet the Dark Absol said you need to take care of cases where the descendent isn't a descendent, e.g.
function getLevel(parent, child) {
let level = 0;
while (child && parent != child) {
level++;
child = child.parentNode;
}
return child? level : null;
}
var parent = document.getElementById('parent');
var child = document.getElementById('child');
var notChild = document.getElementById('notChild');
console.log(getLevel(parent, child)); // 3
console.log(getLevel(parent, notChild)); // null
<div id="parent">
<div>
<div>
<div id="child"></div>
</div>
</div>
</div>
<div id="notChild"></div>
Without the guarding condition to stop when the loop runs out of parnetNodes, it will throw an error if child isn't a descendant of parent.

Get Nth parent of multiple elements

How can I get the the Nth parent of multiple elements?
Right now I do it like this:
var winners = THUMBNAILS.find(".tournament-winner:contains(" + filter + ")");
var filteredThumbnails = winners.parent().parent().parent().parent().clone();
this does work.
But when I try to do it like:
var filteredThumbnails = winners.parents().eq(3).clone();
It only gets the thumbnail (great grandfather) of just one element in the winners variable.
Is there any easier way to get Nth parent of multiple elements?
You can use .add()
Create a new jQuery object with elements added to the set of matched elements.
//Create empty object
var filteredThumbnails = $();
//Iterate and target parent
winners.each(function(){
filteredThumbnails.add($(this).parents().eq(3).clone());
});
write a .map() or .forEach() function on the winners array.
example:
filteredThumbnails = winners.map(winner => winner.parent().parent().parent().parent().clone());
It would be a lot easier if you shared your html.
As i am not sure what you are trying to achieve...but
Another way of getting up multiple parent levels of the element you are targeting,
which doesn't have a unique name would be like below. which will traverse up until it finds a parent which has an id which starts with "parent".
winners.parents("[id^=parent]")
Although i would give them a generic css class to travel up to rather than this.
You can also use filter on return value of parents.
$.parents return an array.
Let's say you have the following HTML and you want to get divs with ids parentN. which is the third parent. After that you can convert the elements to an array. Alternatively, you can create a function like this and use it in different places.
then you have to get every fourth element of the parents
function getNthParents(obj, N) {
return obj.parents().filter(index => !((index + 1) % N)).toArray();
}
nthParents = getNthParents($('.thumbnail'), 3)
console.log(nthParents)
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id='parent1'>
<div>
<div>
<div class='thumbnail'>
</div>
</div>
</div>
</div>
<div id='parent2'>
<div>
<div>
<div class='thumbnail'>
</div>
</div>
</div>
</div>
<div id='parent3'>
<div>
<div>
<div class='thumbnail'>
</div>
</div>
</div>
</div>

CSS - Parent child selector not working

I have the following code:
.recipe
.ingredients
= f.simple_fields_for :ingredients do |ingredient|
= render 'ingredient_fields', f: ingredient
.row#links
.col-xs-12
= link_to_add_association "", f, :ingredients
%hr
I need to select the ingredients div using jquery in the format of $("#links")["closest"](".recipe > .ingredients") but this doesn't select anything.
It's frustrating though as $("#links")["closest"](".recipe > .row") will return the correct div.
Fiddle of what works and what I want: https://jsfiddle.net/yL6dr4s1/
According to jQuery documentation, closest method tries to find element matching the selector by testing the element itself and
traversing up through DOM.
It does not go through siblings of the element.
Based on your requirements, it seems like you want to traverse the tree for getting match in siblings. jQuery has siblings method to do that. So one solution would be to use siblings method like:
$("#links")["siblings"](".recipe > .ingredients")
Another soultion would be to get closest parent and then use children as answered by #mhodges
As for the query $("#links")["closest"](".recipe > .row"):
It works fine because closest method finds the match in the element itself.
Here is the example to showcase that:
$(document).ready(function() {
// Match found because it is parent
console.log($("#links")["closest"](".wrapper").length);
// No match found because element is sibling
console.log($("#links")["closest"](".row1").length);
// No match found because element is sibling
console.log($("#links")["closest"](".row3").length);
// Match found because it is element itself
console.log($("#links")["closest"](".row2").length);
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="wrapper">
<div class="row1">
<span>Content1</span>
</div>
<div class="row2" id="links">
<span>Content2</span>
</div>
<div class="row3">
<span>Content3</span>
</div>
</div>
I am not sure of your requirements on using the exact selector/syntax you provided, but this selector works exactly how you want it to.
$(this).closest(".recipe").children(".ingredients").append('<br/><input type="text" value="Flour">');
Edit
This is the closest I could get:
$(this)["closest"](".recipe").children(".ingredients").append('<br/><input type="text" value="Flour">');
I don't think you can use the selectors in the way you propose.
As far as the DOM is concerned (and jQuery), the element defined by ingredient and the element defined by row are not related. You have to traverse up to the parent element, then back down to get to the child.
Here is a fiddle that hopefully demonstrates the issue.
If you can change it so that ingredient and row are both within the same parent div, you might have more luck with your test selector syntax.
When jQuery gets to buggy, doesn't have a certain option or just becomes to messy to use for a certain operation, it is good we also have access to good old plain javascript.
document.querySelector('#addToIngredients').addEventListener('click' , function(e) {
var recipe = getClosest(e.target,'recipe');
if (recipe) {
var ingred = recipe.querySelector('.ingredients');
ingred.innerHTML += '<br/><input type="text" value="Flour">';
}
});
function getClosest(elem,cls) {
var el = elem.parentNode;
while (el){
if (el.className.indexOf(cls) > -1) {
return el;
}
el = el.parentNode;
}
return false;
}
<div class="recipe">
<div class="ingredients">
<input type="text" value="Eggs"><br/>
<input type="text" value="Flour">
</div>
<div class="row">
<div class="col-xs-12">
Add to .ingredients
</div>
</div>
<hr/>
</div>
Of course they can be combined
$(function() {
$("#addToIngredients").on('click', function(e) {
var recipe = getClosest(e.target,'recipe');
if (recipe) {
var ingred = recipe.querySelector('.ingredients');
ingred.innerHTML += '<br/><input type="text" value="Flour">';
}
});
})

Categories