Find distance between elements in the DOM - javascript

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.

Related

Use .find() to get all descendants of an element but not their descendants

I have some html where elements are nested to create an accordion effect. Something similar to:
<span class="container">
<div class="item">1</div>
<span class="container">
<div class="item">1.a</div>
<span class="container">
<div class="item">1.a.1</div>
</span>
</span>
</span>
I need to use $('.container').find('.item').each() to create a hierarchy where 1.a becomes a child of 1 and 1.a.1 becomes a child of 1.a. The issue that I have is that 1.a.1 also becomes a child of 1 because 1.a.1 is a descendant of 1.
Theoretically there could also be 1.b or 1.c, etc. so I do need to find all descendants, not just the first one, but I don't want to find descendants of descendants.
Is there a simple jQuery way to do this?
.children() doesn't work for my specific case because there are more nested containers and whatnot in the actual HTML I'm in.
I've written the functionality here. The idea is that you're looking for all descendants with the item class. Then you remove all those elements from the list that have an ancestor with the item class. That ancestor still needs to be a descendant from the container (or your element starting point).
var container = $('.container');
var descendants = container.find('.item');
var res = [];
descendants.each(function() {
let parents = $(this).parents();
for (let i = 0; i < parents.length; i++) {
//console.log('p: ' + $(parents[i]).text());
if (container.html() == $(parents[i]).html()) {
//no ancestors with the `item` class found, this is a good one.
res.push($(this));
break;
}
if ($(parents[i]).hasClass('item')) {
//if an ancestor with the same class has been found
//it cannot be kept
break;
}
}
});
console.log(descendants);
console.log(res);
I created a fiddle to test

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

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

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

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.

Finding child element by class from parent with pure javascript cross browser

Following my code:
<div onclick="/*Here I would like to select the child element with the class 'vxf'*/">
<div class="abc"></div>
<div class="cir"></div>
<!--... other elements-->
<div class="vxf"></div>
<!--... other elements-->
</div>
<div onclick="/*Here I would like to select the child element with the class 'vxf'*/">
<div class="abc"></div>
<div class="cir"></div>
<!--... other elements-->
<div class="vxf"></div>
<!--... other elements-->
</div>
How to select the child element with the class "vxf" with pure javascript?
Pass this into your handler...
onclick="clickHandler(this)"
...and then for maximum browser compatibility, just look in the child nodes:
function clickHandler(element) {
var child;
for (child = element.firstNode; child; child = child.nextSibling) {
if (child.className && child.className.match(/\bvxf\b/)) {
break; // Found it
}
}
// ...
}
(Or keep looping and build up an array, if you want all matching children.)
On most modern browsers, another alternative is to use querySelector (to find the first) or querySelectorAll (to get a list) of matching child elements. Sadly, this requires a bit of a trick:
function clickHandler(element) {
var child, needsId;
needsId = !element.id;
if (needsId) {
element.id = "TEMPID____" + (new Date()).getTime();
}
child = document.querySelector("#" + element.id + " > .vxf");
if (needsId) {
element.id = "";
}
// ...
}
We have to play the id game because we only want direct children (not descendants), and unfortunately you can't use a child combinator without something on the left of it (so element.querySelector("> .vxf"); doesn't work).
If you didn't care whether it was a direct child or a descendant, then of course it's a lot easier:
function clickHandler(element) {
var child = element.querySelector(".vxf");
// ...
}
Just use this.getElementsByClassName('vxf')[0] in the div's onclick, and you have the element. See this fiddle.
in HTML5 you can use document.querySelector('.vxf')
As pointed out in other answers you can also use document.getElementsByClassName('vxf') for this specific requirement but the document.querySelector() and document.querySelectorAll() methods allow you to provide more complex selectors and therefore give you more power, so worth looking at for future.
See here for more information.

Categories