29

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?

BoltClock
  • 700,868
  • 160
  • 1,392
  • 1,356
Tomkay
  • 5,120
  • 21
  • 60
  • 92

12 Answers12

52

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
}
Michael Connor
  • 4,182
  • 24
  • 21
  • 1
    This function works well, and in my opinion better than the accepted answer as it allows you to get unique path for ambiguous elements. For example an unordered list with no unique identifiers on the list items. – Adam Aug 06 '14 at 21:28
  • I agree with @Adam, this function targets the correct element in case there is multiple same elements (ul>li:eq(1)>a). Perfect! – Cyril N. Oct 23 '15 at 11:36
  • For my case, I have to check whether the element hasAttribute is a function: `typeof el.hasAttribute === 'function'` before `el.hasAttribute('id')` – The gates of Zion Nov 01 '21 at 13:52
37

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   ">
    <a href="#" class="rightArrow" title="Next image">Click here</a>
</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   ">
    <a href="#" class="rightArrow" title="Next image">Click here</a>
</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.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • 5
    I think you already know that you are a badass. Thanks man! :) – Tomkay Apr 20 '11 at 11:04
  • For your pure JS example, it might be more performant to do `rightArrowParents.unshift()` rather than `push()` and `reverse()`? – p0lar_bear Feb 15 '19 at 16:24
  • @p0lar_bear - I doubt it, since that requires shuffling all the properties every time in the loop rather than once at the end. But I also doubt that it matters given how few there will be. :-) – T.J. Crowder Feb 15 '19 at 16:39
  • 1
    Actually even better `this.className.trim().replace(/ {2,}/g, ' ')` to also replace extra spaces inbetween class names – QuantumBlack Nov 17 '22 at 15:33
  • 1
    @QuantumBlack - Another good point. I went with `entry += className.replace(/ +/g, ".");` on an already-trimmed `className` (since I was trimming it prior to testing if it's empty). – T.J. Crowder Nov 17 '22 at 18:22
13

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(' > ');
}
runejuhl
  • 2,147
  • 1
  • 17
  • 17
Aleksandar Totic
  • 2,557
  • 25
  • 27
  • 1
    Warning: shadow DOM v1 has dropped support for ::shadow selectors. There is no way to generate a full CSS selector for shadow DOM elements. I recommend replacing the "::shadow" line above with an error. – Aleksandar Totic Sep 04 '18 at 17:04
3

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/

Community
  • 1
  • 1
Gabriele Petrioli
  • 191,379
  • 34
  • 261
  • 317
3

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;
};
neurino
  • 11,500
  • 2
  • 40
  • 63
2

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

darthmatch
  • 31
  • 4
2

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
}
whichdan
  • 1,897
  • 1
  • 14
  • 11
2

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)
0
    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);
Haradzieniec
  • 9,086
  • 31
  • 117
  • 212
0

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

0
$(".rightArrow")
  .parents()
  .map(function () { 
      var value = this.tagName.toLowerCase();
      if (this.className) {
          value += '.' + this.className.replace(' ', '.', 'g');
      }
      return value;
  })
  .get().reverse().join(", ");
Pavlo Neiman
  • 7,438
  • 3
  • 28
  • 28
sv_in
  • 13,929
  • 9
  • 34
  • 55
  • 3
    Re `.replace(' ', '.', 'g')`: Standard JavaScript has no third parameter on the `String.prototype.replace` function; see section 15.5.4.11 of [the specification](http://www.ecma-international.org/publications/standards/Ecma-262.htm). You want `.replace(/ /g, '.')` instead. – T.J. Crowder Apr 20 '11 at 10:15
0

This is the Typescript version of Michael Connor's solution. This works well when placed into a service in Angular.

public onClick(target: HTMLHtmlElement) {
  domPath = this.getDomPath(target);
  console.log(`DOM of target: ${JSON.stringify(domPath)}`);
}


private getDomPath(el: HTMLHtmlElement): string[] {
  const stack = [];
  while ( el.parentNode != null ) {
    console.log(el.nodeName);
    let sibCount = 0;
    let sibIndex = 0;
    for (let i = 0; i < el.parentNode.childNodes.length; i++ ) {
      const 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 as HTMLHtmlElement;
  }

  return stack.slice(1); // removes the html element
}

In my instance I have a global click listener. You can use a dedicated (click) handler for your hyperlink which matches OPs question.

TomDK
  • 1,321
  • 11
  • 16