How is it possible to iterate through the HTML DOM and list all nodes with there depth in javascript.
Example:
<div>
<img src="foo.jpg">
<p>
<span>bar</span>
</p>
</div>
would result in
div 0
img 1
p 1
span 2
Write a recursive function which tracks the depth:
function element_list(el,depth) {
console.log(el+' '+depth);
for(var i=0; i<el.children.length; i++) {
element_list(el.children[i],depth+1);
}
}
element_list(document,0);
As CodeiSir points out, this will also list text nodes, but we can filter them out by testing the nodeType. Variations on this code will allow/ignore other node types as desired.
function element_list(el,depth) {
if (el.nodeType === 3) return;
Note that the other answers are/where not realy correct ...
This will also filter out "TEXT" Nodes, and not output the BODY tag.
function getDef(element, def) {
var str = ""
var childs = element.childNodes
for (var i = 0; i < childs.length; ++i) {
if (childs[i].nodeType != 3) {
str += childs[i].nodeName + " " + def + "<br />"
str += getDef(childs[i], def + 1)
}
}
return str
}
// Example
document.body.innerHTML = getDef(document.body, 0)
<div>
<img src="foo.jpg">
<p>
<span>bar</span>
</p>
</div>
Yes, you can! You would have to iterate and some logic to create this tree, but for your example, you could do something like:
var tracker = {};
Array.from(document.querySelectorAll("*")).forEach(node => {
if (!tracker[node.tagName]) tracker[node.tagName] = 1;
else tracker[node.tagName]++;
});
console.log(tracker);
You can modify this to run on a recrusive subset of childNodes. This just iterates the entire document.
Check this fiddle and open the console to see the output of tracker which counts and lists tag names. To add the depth, just grab the parentNode.length all the way up.
Here's an updated script which I think does the depth count propery;
var tracker = {};
var depth = 0;
var prevNode;
Array.from(document.querySelectorAll("*")).forEach(node => {
if (!tracker[node.tagName]) tracker[node.tagName] = 1;
else tracker[node.tagName]++;
console.log("Node depth:", node.tagName, depth);
if (node.parentNode != prevNode) depth++;
prevNode = node;
});
console.log(tracker);
getElementDepth returns the absolute depth of the node (starting from the html node), to get the difference of depth between two nodes you can just subtract an absolute depth from another.
function getElementDepthRec(element,depth)
{
if(element.parentNode==null)
return depth;
else
return getElementDepthRec(element.parentNode,depth+1);
}
function getElementDepth(element)
{
return getElementDepthRec(element,0);
}
function clickEvent() {
alert(getElementDepth(document.getElementById("d1")));
}
<!DOCTYPE html>
<html>
<body>
<div>
<div id="d1">
</div>
</div>
<button onclick="clickEvent()">calculate depth</button>
</body>
</html>
My original solution walked each element up the DOM using a while loop to determine its depth:
var el = document.querySelectorAll('body *'), //all element nodes, in document order
depth,
output= document.getElementById('output'),
obj;
for (var i = 0; i < el.length; i++) {
depth = 0;
obj = el[i];
while (obj.parentNode !== document.body) { //walk the DOM
depth++;
obj = obj.parentNode;
}
output.textContent+= depth + ' ' + el[i].tagName + '\n';
}
<div>
<img src="foo.jpg">
<p>
<span>bar</span>
</p>
</div>
<hr>
<pre id="output"></pre>
I've come up with a new solution, which stores the depths of each element in an object. Since querySelectorAll() returns elements in document order, parent nodes always appear before child nodes. So a child node's depth can be calculated as the depth of its parent node plus one.
This way, we can determine the depths in a single pass without recursion:
var el = document.querySelectorAll('body *'), //all element nodes, in document order
depths= { //stores the depths of each element
[document.body]: -1 //initialize the object
},
output= document.getElementById('output');
for (var i = 0; i < el.length; i++) {
depths[el[i]] = depths[el[i].parentNode] + 1;
output.textContent+= depths[el[i]] + ' ' + el[i].tagName + '\n';
}
<div>
<img src="foo.jpg">
<p>
<span>bar</span>
</p>
</div>
<hr>
<pre id="output"></pre>
Anyone looking for something which iterates through the tree under a node without using recursion* but which also gives you depth (relative to the head node) ... as well as sibling-ancestor coordinates at all times:
function walkDOM( headNode ){
const stack = [ headNode ];
const depthCountDowns = [ 1 ];
while (stack.length > 0) {
const node = stack.pop();
console.log( '\ndepth ' + ( depthCountDowns.length - 1 ) + ', node: ');
console.log( node );
let lastIndex = depthCountDowns.length - 1;
depthCountDowns[ lastIndex ] = depthCountDowns[ lastIndex ] - 1;
if( node.childNodes.length ){
depthCountDowns.push( node.childNodes.length );
stack.push( ... Array.from( node.childNodes ).reverse() );
}
while( depthCountDowns[ depthCountDowns.length - 1 ] === 0 ){
depthCountDowns.splice( -1 );
}
}
}
walkDOM( el );
PS it will be understood that I've put in > 0 and === 0 to try to improve clarity... first can be omitted and second can be replaced with leading ! of course.
* look here for the appalling truth about the cost of recursion in JS (contemporary implementations at 2018-02-01 anyway!)
You can do this by using Jquery
$('#divId').children().each(function () {
// "this" is the current element
});
and the html should be like the following:
<div id="divId">
<img src="foo.jpg">
<p>
<span>bar</span>
</p>
(() => {
const el = document.querySelectorAll('body *');
const depths = new Map();
depths.set(document.body, -1)
el.forEach((e) => {
const p = e.parentNode;
const d = depths.get(p);
depths.set(e, d + 1);
})
return depths;
})()
This is Rick Hitchcocks answer but using Map instead of object
Results in Map(5) {body => -1, div => 0, img => 1, p => 1, span => 2}
Related
I currently have a working variable that targets the parent node 3 levels up
searchClearSelector: '.search-form__clear', //The clear button on my input
//Checks if there is a value in the input to clear then calls a function
self._ClearActions = document.querySelectorAll(self.searchClearSelector);
if (self._ClearActions && self._ClearActions.length > 0) {
for (const _Action of self._ClearActions) {
_Action.addEventListener('click', self.ClearActionHandler);
}
}
//Clear function
ClearActionHandler() {
const self = this;
const _Handle = this;
const _HandleParent = parentNode.parentNode.parentNode;
const _SearchInput = _HandleParent.querySelector('.search-form__input');
}
<input className="search-form__input" placeholder="Enter text" />
<div className="search-form__actions">
<button type="button" className="search-form__clear">Clear</button>
</div>
It works fine, but seems a little hacky - is there a better way for me to do this?
Thanks!
EDIT - Updated code, i had to take a big chunk of the other stuff out as it is a huge file, the issue is with IE not clearing it (as it doesn't work with closest in vanilla JS)
You can implement a function like this:
const _HandleParent = parentsUntil(3, this); // 3rd parent
const parentsUntil = (num, elem) => {
if(num <= 0) return;
let node = elem;
while(num--) node = node.parentNode;
return node;
}
You could use a recursive function:
function upperParents(element, levels) {
if (levels == 1) return element.parentNode || element;
if (levels < 1) return element;
return upperParents(element.parentNode || element, levels - 1);
}
document.getElementById('btnwithparents').addEventListener('click', function() {
console.log(upperParents(this, 2));
});
<div class="theparent">
<span>
<button id="btnwithparents">Search my parent 2 levels above</button>
</span>
</div>
You can do this with while loop.
var node = document.getElementById("id");
var levels = 3;
while (levels >= 0) {
node = node.parentNode;
levels--;
}
Everyone gave solutions, here's another:
function getUpstream(el,lvl){
if(!(el instanceof Node)){
throw("Not a node!");
}
while((--lvl>=0)&&el){
el = el.parentNode;
}
return el;
}
getUpstream(document.body,1)
Today in class we were given a task to 'recreate' the jQuery functionality (I don't know why, I guess the teacher is sadistic).
We were given the following instruction:
Using only pure javascript (not even ECMEScript 2015) create a function '$' that will receive a tag / id / class.
If you're given only one token [ e.g $("#id)] return the suitable elements.
If you're given more than one token [ e.g $("nav div div p")]- search hierarchically for the last token.
Input: $('nav div p');
Goal: Get all the p elements that has a div above them which has a nav above the div.
if (query.split(" ").length > 1) {
var first = query.split(" ")[0];
var rest = query.split(" ").slice(1);
var curr_elements;
if (first.match(/^#.*/i)) {
curr_elements = (document.getElementById(first.substring(1)));
} else if (query.match(/^\..*/i)) {
curr_elements = document.getElementsByClassName(first.substring(1));
} else if (query.match(/^\w.*/i)) {
curr_elements = document.getElementsByTagName(first);
}
curr_elements = [].slice.call(curr_elements);
for (var e = 0; e < curr_elements.length; e++) {
if (curr_elements[e].hasChildNodes()) {
for (var i = 0; i < rest.length; i++) {
var temp = rest[i];
var children;
if (temp.match(/^#.*/i)) {
children = (document.getElementById(temp.substring(1)));
} else if (temp.match(/^\..*/i)) {
children = document.getElementsByClassName(temp.substring(1));
} else if (temp.match(/^\w.*/i)) {
children = document.getElementsByTagName(temp);
}
alert(children);
//curr_elements += children;
}
}
}
this.elements = curr_elements;
} else {
if (query.match(/^#.*/i)) {
this.elements.push(document.getElementById(query.substring(1)));
} else if (query.match(/^\..*/i)) {
this.elements = document.getElementsByClassName(query.substring(1));
} else if (query.match(/^\w.*/i)) {
this.elements = document.getElementsByTagName(query);
}
}
<nav>
<p id="1"></p>
</nav>
<nav>
<div>
</div>
<p>
<div>
<p id=2></p>
</div>
</p>
</nav>
<nav>
<div>
<p id="3"></p>
<p id="4"></p>
<div>
</div>
</div>
</nav>
<nav>
<div>
<div>
<div>
<p id="5"></p>
</div>
</div>
</div>
</nav>
Variables:
query: the first arg, is tag / id / class name.
curr_elements: array-to-be for temporarily storing the elements I get.
this.elements: the final HTMLCollection.
The part relating to single token ($("p");) works fine, but I'm having trouble figuring out how to recurse / iterate over the elements to get the paragraphs.
Hoping to get someone's idea / advice on how to continue.
Your code is fairly solid, you just are not using recursion which in this case is quite useful.
I've changed your code to use a recursive approach:
function $(selector, context) {
if (!selector) return false;
// context should be an array of previous nodes we have found. If it's undefined, assume the single-item array [document] as the starting context
if (!context) context = [document];
var s = selector.split(" ");
var first = s.shift();
var curr_elements = [], els;
for (var i=0; i < context.length; i++) {
var c = context[i];
// make sure els gets converted into a real array of nodes
if (first.match(/^#.*/i)) {
els = [c.getElementById(first.substring(1))];
} else if (first.match(/^\..*/i)) {
els = [].slice.call(c.getElementsByClassName(first.substring(1)));
} else if (first.match(/^\w.*/i)) {
els = [].slice.call(c.getElementsByTagName(first));
}
curr_elements = curr_elements.concat(els);
}
// if there are more items in s, then curr_elements is the context in which to find them. Otherwise, curr_elements is the array of elements we were looking for.
if (s.length) return $(s.join(" "), curr_elements);
return curr_elements;
}
var a = $(".test span");
console.log(a);
<div class="test">
<span>1</span><span>2</span><span>3</span>
</div>
<div class="not_test">
<span>4</span><span>5</span><span>6</span>
</div>
<p class="test">
<span>7</span><span>8</span><span>9</span>
</p>
I am getting the child tree name but I want to get its complete hierarchy of parent node names.
Below code shows how I get the child node and print its value in a particular div element:
$(document).ready(function () {
$('#bdeViewNew').on('changed.jstree', function (e, data) {
var i, j, r = [];
for (i = 0, j = data.selected.length; i < j; i++) {
r.push(data.instance.get_node(data.selected[i]).text.trim());
$('#treeBreadCrumbs').html(r.join(', '));
}
});
});
Now it prints the value of child node, e.g. Child a. But I want something like follows, if the tree structure is as shown below:
Parent
Child 1
Child a
Child b
Child 2
Child c
Child d
so if I click on Child a I want my div content updated as
Parent > Child 1 > Child a
as of now I am getting Child a only. Please let me know how I can get the correct output.
I tried like this as shown below to get path of all the parent node:
$(document).ready(function() {
$('#bdeViewNew').on('changed.jstree', function(e, data) {
var ids = data.inst.get_path('#bdeViewNew' + data.rslt.obj.attr('id'),true);
var names = data.inst.get_path('#bdeViewNew' + data.rslt.obj.attr('id'),false);
alert("Path [ID or Name] from root node to selected node = ID's = "+ids+" :: Name's = "+names);
});
});
but still no result to get_path. Do I need to use different JS or a plugin? And what is the meaning of attr('id') i should pass the id of that li or something else as i did'nt understand this syntax properly.
Adding my jstree:
<div id="bdeViewNew">
<ul>
<li id="bde" data-jstree='{"opened":true,"icon":"./images/tree.png"}'">
Parent 1
<ul>
<li id="aaa" data-jstree='{"opened":true,"icon":"./images/tree.png"}'>
Child 1 <c:forEach items="${empInfo.empList}"
var="empValue">
<ul>
<li id="bbb" data-jstree='{"icon":"./images/tree.png"}' >${empValue.empName}</li>
</ul>
</c:forEach>
</li>
</ul>
<ul>
<li id="ccc" data-jstree='{"icon":"./images/tree.png"}'>Child 2
<c:forEach items="${imgInfo.imgList}"
var="imgValue">
<ul>
<li id="ddd" data-jstree='{"icon":"./images/tree.png"}'>${imgValue.imgName}</li>
</ul>
</c:forEach>
</li>
</ul>
</li>
</ul>
</div>
This will work fine... it will get the full parents...
$('#bdeViewNew').on('select_node.jstree', function (e, data) {
var loMainSelected = data;
uiGetParents(loMainSelected);
});
function uiGetParents(loSelectedNode) {
try {
var lnLevel = loSelectedNode.node.parents.length;
var lsSelectedID = loSelectedNode.node.id;
var loParent = $("#" + lsSelectedID);
var lsParents = loSelectedNode.node.text + ' >';
for (var ln = 0; ln <= lnLevel -1 ; ln++) {
var loParent = loParent.parent().parent();
if (loParent.children()[1] != undefined) {
lsParents += loParent.children()[1].text + " > ";
}
}
if (lsParents.length > 0) {
lsParents = lsParents.substring(0, lsParents.length - 1);
}
alert(lsParents);
}
catch (err) {
alert('Error in uiGetParents');
}
}
var node=$('#drives_tree').jstree("get_selected", true);
$("#breadcrumbs").text($('#drives_tree').jstree().get_path(node[0], ' > '));
We have direct method to get the parent information.
$('#bdeViewNew').on('select_node.jstree', function (e, data) {
var loMainSelected = data;
alert(loMainSelected.node.parents);
});
It will return ["Child1", "Parent"] . Using this method we can get the all the parents up to root.
function uiGetParents(loSelectedNode) {
try {
var loData = [];
var lnLevel = loSelectedNode.node.parents.length;
var lsSelectedID = loSelectedNode.node.id;
var loParent = $("#" + lsSelectedID);
var lsParents = loSelectedNode.node.text + ' >';
for (var ln = 0; ln <= lnLevel - 1 ; ln++) {
var loParent = loParent.parent().parent();
if (loParent.children()[1] != undefined) {
lsParents += loParent.children()[1].text + " > ";
loData.push(loParent.children()[1].text);
}
}
if (lsParents.length > 0) {
lsParents = lsParents.substring(0, lsParents.length - 1);
}
alert(lsParents);
alert(loData.reverse());
}
catch (err) {
alert('Error in uiGetParents');
}
}
The result stored in array loData . and just reverse the data and loop the loData Array. You got u r output
**
Step 1 : Get the Selected node Id and selected node level
Step 2 : Using node id get the current parent like this ( ex.. $("#101000000892").parent().parent() )
step 2.2 : Next level node ( $("#101000000892").parent().parent().parent().parent() )
step 2.3 : stroe this in a vairable var loNode = $("#101000000892").parent().parent();
step 2.4 : Next you can loNode.parent().parent()
Step 3 : Using for loop u can loop through the level until reached 1.
step 4 : Now you can get the full text .
**
Try this,
Instead of angular.each, use a for loop.
You will get an arry with all the text of the nodes.
var breadcrumbArr = [selectedNode.node.text];
angular.forEach(selectedNode.node.parents, function(element, index) {
if (element === '#') return;
breadcrumbArr.push($('#'+element).find('a:first').text())
});
breadcrumbArr.reverse();
return breadcrumbArr;
A slightly less verbose version that prints Parent > Child
function uiGetParents(node) {
try {
var level = node.node.parents.length;
var elem = $('#' + node.node.id);
var text = node.node.text;
for (var ln = 0; ln <= level - 1 ; ln++) {
elem = elem.parent().parent();
var child = elem.children()[1];
if (child != undefined) {
text = child.text + ' > ' + text;
}
}
console.log(text);
}
catch (err) {
console.log('Error in uiGetParents');
}
}
var node = $tree.jstree().get_node(id),
formatted_name = $tree.jstree().get_text(node);
$.each(node.parents, function(key, parentId) {
if ( parentId !== "#" ) {
var parent = $tree.jstree().get_node(parentId);
formatted_name = $tree.jstree().get_text(parent) + "->" + formatted_name;
}
});
console.log(formatted_name);
HTML
<body>
<div class="lol">
<a class="rightArrow" href="javascriptVoid:(0);" title"Next image">
</div>
</body>
Pseudo Code
$(".rightArrow").click(function() {
rightArrowParents = this.dom(); //.dom(); is the pseudo function ... it should show the whole
alert(rightArrowParents);
});
Alert message would be:
body div.lol a.rightArrow
How can I get this with javascript/jquery?
Here is a native JS version that returns a jQuery path. I'm also adding IDs for elements if they have them. This would give you the opportunity to do the shortest path if you see an id in the array.
var path = getDomPath(element);
console.log(path.join(' > '));
Outputs
body > section:eq(0) > div:eq(3) > section#content > section#firehose > div#firehoselist > article#firehose-46813651 > header > h2 > span#title-46813651
Here is the function.
function getDomPath(el) {
var stack = [];
while ( el.parentNode != null ) {
console.log(el.nodeName);
var sibCount = 0;
var sibIndex = 0;
for ( var i = 0; i < el.parentNode.childNodes.length; i++ ) {
var sib = el.parentNode.childNodes[i];
if ( sib.nodeName == el.nodeName ) {
if ( sib === el ) {
sibIndex = sibCount;
}
sibCount++;
}
}
if ( el.hasAttribute('id') && el.id != '' ) {
stack.unshift(el.nodeName.toLowerCase() + '#' + el.id);
} else if ( sibCount > 1 ) {
stack.unshift(el.nodeName.toLowerCase() + ':eq(' + sibIndex + ')');
} else {
stack.unshift(el.nodeName.toLowerCase());
}
el = el.parentNode;
}
return stack.slice(1); // removes the html element
}
Using jQuery, like this (followed by a solution that doesn't use jQuery except for the event; lots fewer function calls, if that's important):
$(".rightArrow").click(function () {
const rightArrowParents = [];
$(this)
.parents()
.addBack()
.not("html")
.each(function () {
let entry = this.tagName.toLowerCase();
const className = this.className.trim();
if (className) {
entry += "." + className.replace(/ +/g, ".");
}
rightArrowParents.push(entry);
});
console.log(rightArrowParents.join(" "));
return false;
});
Live example:
$(".rightArrow").click(function () {
const rightArrowParents = [];
$(this)
.parents()
.addBack()
.not("html")
.each(function () {
let entry = this.tagName.toLowerCase();
const className = this.className.trim();
if (className) {
entry += "." + className.replace(/ +/g, ".");
}
rightArrowParents.push(entry);
});
console.log(rightArrowParents.join(" "));
return false;
});
<div class=" lol multi ">
Click here
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
(In the live examples, I've updated the class attribute on the div to be lol multi to demonstrate handling multiple classes.)
That uses parents to get the ancestors of the element that was clicked, removes the html element from that via not (since you started at body), then loops through creating entries for each parent and pushing them on an array. Then we use addBack to add the a back into the set, which also changes the order of the set to what you wanted (parents is special, it gives you the parents in the reverse of the order you wanted, but then addBack puts it back in DOM order). Then it uses Array#join to create the space-delimited string.
When creating the entry, we trim className (since leading and trailing spaces are preserved, but meaningless, in the class attribute), and then if there's anything left we replace any series of one or more spaces with a . to support elements that have more than one class (<p class='foo bar'> has className = "foo bar", so that entry ends up being p.foo.bar).
Just for completeness, this is one of those places where jQuery may be overkill, you can readily do this just by walking up the DOM:
$(".rightArrow").click(function () {
const rightArrowParents = [];
for (let elm = this; elm; elm = elm.parentNode) {
let entry = elm.tagName.toLowerCase();
if (entry === "html") {
break;
}
const className = elm.className.trim();
if (className) {
entry += "." + className.replace(/ +/g, ".");
}
rightArrowParents.push(entry);
}
rightArrowParents.reverse();
console.log(rightArrowParents.join(" "));
return false;
});
Live example:
$(".rightArrow").click(function () {
const rightArrowParents = [];
for (let elm = this; elm; elm = elm.parentNode) {
let entry = elm.tagName.toLowerCase();
if (entry === "html") {
break;
}
const className = elm.className.trim();
if (className) {
entry += "." + className.replace(/ +/g, ".");
}
rightArrowParents.push(entry);
}
rightArrowParents.reverse();
console.log(rightArrowParents.join(" "));
return false;
});
<div class=" lol multi ">
Click here
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
There we just use the standard parentNode property (or we could use parentElement) of the element repeatedly to walk up the tree until either we run out of parents or we see the html element. Then we reverse our array (since it's backward to the output you wanted), and join it, and we're good to go.
I needed a native JS version, that returns CSS standard path (not jQuery), and deals with ShadowDOM. This code is a minor update on Michael Connor's answer, just in case someone else needs it:
function getDomPath(el) {
if (!el) {
return;
}
var stack = [];
var isShadow = false;
while (el.parentNode != null) {
// console.log(el.nodeName);
var sibCount = 0;
var sibIndex = 0;
// get sibling indexes
for ( var i = 0; i < el.parentNode.childNodes.length; i++ ) {
var sib = el.parentNode.childNodes[i];
if ( sib.nodeName == el.nodeName ) {
if ( sib === el ) {
sibIndex = sibCount;
}
sibCount++;
}
}
// if ( el.hasAttribute('id') && el.id != '' ) { no id shortcuts, ids are not unique in shadowDom
// stack.unshift(el.nodeName.toLowerCase() + '#' + el.id);
// } else
var nodeName = el.nodeName.toLowerCase();
if (isShadow) {
nodeName += "::shadow";
isShadow = false;
}
if ( sibCount > 1 ) {
stack.unshift(nodeName + ':nth-of-type(' + (sibIndex + 1) + ')');
} else {
stack.unshift(nodeName);
}
el = el.parentNode;
if (el.nodeType === 11) { // for shadow dom, we
isShadow = true;
el = el.host;
}
}
stack.splice(0,1); // removes the html element
return stack.join(' > ');
}
Here is a solution for exact matching of an element.
It is important to understand that the selector (it is not a real one) that the chrome tools show do not uniquely identify an element in the DOM. (for example it will not distinguish between a list of consecutive span elements. there is no positioning/indexing info)
An adaptation from a similar (about xpath) answer
$.fn.fullSelector = function () {
var path = this.parents().addBack();
var quickCss = path.get().map(function (item) {
var self = $(item),
id = item.id ? '#' + item.id : '',
clss = item.classList.length ? item.classList.toString().split(' ').map(function (c) {
return '.' + c;
}).join('') : '',
name = item.nodeName.toLowerCase(),
index = self.siblings(name).length ? ':nth-child(' + (self.index() + 1) + ')' : '';
if (name === 'html' || name === 'body') {
return name;
}
return name + index + id + clss;
}).join(' > ');
return quickCss;
};
And you can use it like this
console.log( $('some-selector').fullSelector() );
Demo at http://jsfiddle.net/gaby/zhnr198y/
The short vanilla ES6 version I ended up using:
Returns the output I'm used to read in Chrome inspector e.g body div.container input#name
function getDomPath(el) {
let nodeName = el.nodeName.toLowerCase();
if (el === document.body) return 'body';
if (el.id) nodeName += '#' + el.id;
else if (el.classList.length)
nodeName += '.' + [...el.classList].join('.');
return getDomPath(el.parentNode) + ' ' + nodeName;
};
I moved the snippet from T.J. Crowder to a tiny jQuery Plugin. I used the jQuery version of him even if he's right that this is totally unnecessary overhead, but i only use it for debugging purpose so i don't care.
Usage:
Html
<html>
<body>
<!-- Two spans, the first will be chosen -->
<div>
<span>Nested span</span>
</div>
<span>Simple span</span>
<!-- Pre element -->
<pre>Pre</pre>
</body>
</html>
Javascript
// result (array): ["body", "div.sampleClass"]
$('span').getDomPath(false)
// result (string): body > div.sampleClass
$('span').getDomPath()
// result (array): ["body", "div#test"]
$('pre').getDomPath(false)
// result (string): body > div#test
$('pre').getDomPath()
Repository
https://bitbucket.org/tehrengruber/jquery.dom.path
I've been using Michael Connor's answer and made a few improvements to it.
Using ES6 syntax
Using nth-of-type instead of nth-child, since nth-of-type looks for children of the same type, rather than any child
Removing the html node in a cleaner way
Ignoring the nodeName of elements with an id
Only showing the path until the closest id, if any. This should make the code a bit more resilient, but I left a comment on which line to remove if you don't want this behavior
Use CSS.escape to handle special characters in IDs and node names
~
export default function getDomPath(el) {
const stack = []
while (el.parentNode !== null) {
let sibCount = 0
let sibIndex = 0
for (let i = 0; i < el.parentNode.childNodes.length; i += 1) {
const sib = el.parentNode.childNodes[i]
if (sib.nodeName === el.nodeName) {
if (sib === el) {
sibIndex = sibCount
break
}
sibCount += 1
}
}
const nodeName = CSS.escape(el.nodeName.toLowerCase())
// Ignore `html` as a parent node
if (nodeName === 'html') break
if (el.hasAttribute('id') && el.id !== '') {
stack.unshift(`#${CSS.escape(el.id)}`)
// Remove this `break` if you want the entire path
break
} else if (sibIndex > 0) {
// :nth-of-type is 1-indexed
stack.unshift(`${nodeName}:nth-of-type(${sibIndex + 1})`)
} else {
stack.unshift(nodeName)
}
el = el.parentNode
}
return stack
}
All the examples from other ответов did not work very correctly for me, I made my own, maybe my version will be more suitable for the rest
const getDomPath = element => {
let templateElement = element
, stack = []
for (;;) {
if (!!templateElement) {
let attrs = ''
for (let i = 0; i < templateElement.attributes.length; i++) {
const name = templateElement.attributes[i].name
if (name === 'class' || name === 'id') {
attrs += `[${name}="${templateElement.getAttribute(name)}"]`
}
}
stack.push(templateElement.tagName.toLowerCase() + attrs)
templateElement = templateElement.parentElement
} else {
break
}
}
return stack.reverse().slice(1).join(' > ')
}
const currentElement = document.querySelectorAll('[class="serp-item__thumb justifier__thumb"]')[7]
const path = getDomPath(currentElement)
console.log(path)
console.log(document.querySelector(path))
console.log(currentElement)
var obj = $('#show-editor-button'),
path = '';
while (typeof obj.prop('tagName') != "undefined"){
if (obj.attr('class')){
path = '.'+obj.attr('class').replace(/\s/g , ".") + path;
}
if (obj.attr('id')){
path = '#'+obj.attr('id') + path;
}
path = ' ' +obj.prop('tagName').toLowerCase() + path;
obj = obj.parent();
}
console.log(path);
hello this function solve the bug related to current element not show in the path
check this now
$j(".wrapper").click(function(event) {
selectedElement=$j(event.target);
var rightArrowParents = [];
$j(event.target).parents().not('html,body').each(function() {
var entry = this.tagName.toLowerCase();
if (this.className) {
entry += "." + this.className.replace(/ /g, '.');
}else if(this.id){
entry += "#" + this.id;
}
entry=replaceAll(entry,'..','.');
rightArrowParents.push(entry);
});
rightArrowParents.reverse();
//if(event.target.nodeName.toLowerCase()=="a" || event.target.nodeName.toLowerCase()=="h1"){
var entry = event.target.nodeName.toLowerCase();
if (event.target.className) {
entry += "." + event.target.className.replace(/ /g, '.');
}else if(event.target.id){
entry += "#" + event.target.id;
}
rightArrowParents.push(entry);
// }
where $j = jQuery Variable
also solve the issue with .. in class name
here is replace function :
function escapeRegExp(str) {
return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
}
function replaceAll(str, find, replace) {
return str.replace(new RegExp(escapeRegExp(find), 'g'), replace);
}
Thanks
$(".rightArrow")
.parents()
.map(function () {
var value = this.tagName.toLowerCase();
if (this.className) {
value += '.' + this.className.replace(' ', '.', 'g');
}
return value;
})
.get().reverse().join(", ");
Given the following HTML-Fragment:
<div>
<p>
abc <span id="x">[</span> def <br /> ghi
</p>
<p>
<strong> jkl <span id="y">]</span> mno </strong>
</p>
</div>
I need an algorithm to fetch all nodes of type Text between #x and #y with Javascript. Or is there a JQuery function that does exactly that?
The resulting Text nodes (whitespace nodes ignored) for the example above would then be:
['def', 'ghi', 'jkl']
The following works in all major browsers using DOM methods and no library. It also ignores whitespace text nodes as mentioned in the question.
Obligatory jsfiddle: http://jsfiddle.net/timdown/a2Fm6/
function getTextNodesBetween(rootNode, startNode, endNode) {
var pastStartNode = false, reachedEndNode = false, textNodes = [];
function getTextNodes(node) {
if (node == startNode) {
pastStartNode = true;
} else if (node == endNode) {
reachedEndNode = true;
} else if (node.nodeType == 3) {
if (pastStartNode && !reachedEndNode && !/^\s*$/.test(node.nodeValue)) {
textNodes.push(node);
}
} else {
for (var i = 0, len = node.childNodes.length; !reachedEndNode && i < len; ++i) {
getTextNodes(node.childNodes[i]);
}
}
}
getTextNodes(rootNode);
return textNodes;
}
var x = document.getElementById("x"),
y = document.getElementById("y");
var textNodes = getTextNodesBetween(document.body, x, y);
console.log(textNodes);
The following example uses jQuery to find any two elements that are next to each other and may or may not have text nodes between them. This foreach loop will check the resulted elements to find any text nodes and add them to the list.
function getTextNodes() {
var list = [];
$(document.body).find("*+*").toArray().forEach(function (el) {
var prev = el.previousSibling;
while (prev != null && prev.nodeType == 3) {
list.push(prev);
prev = prev.previousSibling;
}
});
return list;
}