Skip to content

Instantly share code, notes, and snippets.

@karlgroves
Created November 19, 2013 12:24
Show Gist options
  • Star 23 You must be signed in to star a gist
  • Fork 8 You must be signed in to fork a gist
  • Save karlgroves/7544592 to your computer and use it in GitHub Desktop.
Save karlgroves/7544592 to your computer and use it in GitHub Desktop.
Get DOM path of an element
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
}
//Usage:
var path = getDomPath(document.getElementById('button'));
console.log(path.join(' > '));
@zuluaica18
Copy link

add break; after sibIndex = subCount; and perfect

@jhyland87
Copy link

jhyland87 commented Nov 12, 2023

In case anyone needs an updated version of this, here's what I have:

//https://gist.github.com/karlgroves/7544592

function getDomPath(el, noVerify) {
  // store the original element if verify is enabled. If it isn't, then don't even bother 
  // taking up any memory for it.

  const origElem = el;

  if ( ! el ) {
    console.error('No element provided');
    return;
  }

  const stack = [];
  let levelCount = 0;
  let nearestElemWithId = null;

  let sibParent;
  let sibSiblings;

  do {
  	levelCount++;

    let sibCount = 0;
    let sibIndex = 0;
    sibParent = el?.parentNode;
    sibSiblings = sibParent?.children;

    if ( sibSiblings ){
    	sibSiblings = Array.from(sibSiblings).filter( sibElem => el.nodeName == sibElem.nodeName );
    }

    // Iterate over the childNodes of the elements parentNode to get the
    // index to use
    if ( sibSiblings ){
	    for ( let i = 0; i < sibSiblings.length; i++ ) {
	      let sib = sibSiblings[i];

	      //if ( sib.nodeName != el.nodeName )  continue;
	      
	        sibCount++;

	        if ( sib === el ) {
	          // If this is the correct element, then save the sibIndex
	          // and stop looping
	          sibIndex = sibCount;
	          break;
	        }
	    }
	  }

    if ( el && el.hasAttribute('id') && el.id != '' ) {
      nearestElemWithId = el.id;

      // Turns out, if you have an id that starts with a numerical value, then you can't
      // use it in querySelector[All] unless you either escape it or add [id=] to it.
      if ( /^[0-9]/.test(el.id) ){
      	stack.unshift(`[id="${el.id}"]`);
      }
      else {
      	stack.unshift(`#${el.id}`);
      }
    } 
    else if ( sibCount > 1 ) {
      stack.unshift(el.nodeName.toLowerCase() + ':nth-of-type(' + parseInt(sibIndex) + ')');
    } 
    else {
      stack.unshift(el.nodeName.toLowerCase());
    }

    el = sibParent;
  }
  while( sibParent?.nodeType === Node.ELEMENT_NODE && nearestElemWithId === null );


  if ( stack[0] === 'html' )
  	stack.shift();

  const result = stack.join(' > ');

  if ( noVerify ) return result;

  let selectionFromResult;

  try {
    selectionFromResult = document.querySelector(result);
  }
  catch(err){
    console.error(`Encountered an exception when trying to verify querySelector(${result})\n\tError:`, err);
  }

  // If there's no matches when using querySelector() with the result string, then
  // return false;
  if ( ! selectionFromResult ){
    console.error(`Failed to find any document using querySelector(${result})`);
    return false;
  }

  // If there is a result, but its not the same element, then return false;
  else if ( ! origElem.isSameNode(selectionFromResult) ){
    console.error(`Element returned from querySelector(${result}) is not the same as the element provided`);
  }

  // If we got here, then the matched element is the same element, then it's been verified.
  return result;
}

//Usage:
let elem = document.getElementsByTagName('div')[3];
let elemPath = getDomPath(elem);
console.log('Path to elem is:',elemPath);

Changes:

  • I was getting errors when trying to use a selector with the :eq(n), so I changed it to :nth-type(n) and filtered the list of siblings for the same node type, then increment n in the beginning of the for loop instead of after (since nth-child and nth-type starts at 1, not 0).
  • It will stop traversing over the parents if it finds one with an ID, since you can then start right from there in the path.
  • It will verify the path with the element before returning it (this can be disabled by passing true to 2nd param)

I'm sure there's plenty of room for improvement, but this works great for me in Chrome (I use it a lot for Tampermonkey scripts).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment