views:

110

answers:

5

Let's say I have a large HTML file with different kinds of tags, similar to the StackOverflow one you're looking at right now.

Now let's say you click an element on the page, what would the Javascript function look like that calculates the most basic XPath that refers to that specific element?

I know there are an infinite ways of refering to that element in XPath, but I'm looking for something that just looks at the DOM tree, with no regard for IDs, classes, etc.

Example:

<html>
<head><title>Fruit</title></head>
<body>
<ol>
  <li>Bananas</li>
  <li>Apples</li>
  <li>Strawberries</li>
</ol>
</body>
</html>

Let's say you click on Apples. The Javascript function would return the following:

/html/body/ol/li[2]

It would basically just work its way upward the DOM tree all the way to the HTML element.

Just to clarify, the 'on-click' event-handler isn't the problem. I can make that work. I'm just not sure how to calculate the element's position within the DOM tree and represent it as an XPath.

PS Any answer with or without the use of the JQuery library is appreciated.

PPS I completely new to XPath, so I might even have made a mistake in the above example, but you'll get the idea.

Edit at August 11, 2010: Looks like somebody else asked a similar question: http://stackoverflow.com/questions/2537224/generate-get-the-xpath-for-a-selected-textnode/3458558#3458558

+1  A: 

Firebug can do this, and it's open source (BSD) so you can reuse their implementation, which does not require any libraries.

Matthew Flaschen
Works perfectly! Thanks
Marc
+1  A: 

There is nothing built in to get the xpath of an HTML element, but the reverse is common for example using the jQuery xpath selector.

If you need to determine the xpath of an HTML element you will have to provide a custom function to do this. Here are a couple of example javascript/jQuery impls to calculate the xpath.

krock
I'm actually using jQuery's XPath selector as well, but needed a way to have the user generate an XPath himself.The second page you linked to has some nice examples of this. Thanks!
Marc
+1  A: 

A function I use to get an XPath similar to your situation, it uses jQuery:

function getXPath( element )
{
    var xpath = '';
    for ( ; element && element.nodeType == 1; element = element.parentNode )
    {
        var id = $(element.parentNode).children(element.tagName).index(element) + 1;
        id > 1 ? (id = '[' + id + ']') : (id = '');
        xpath = '/' + element.tagName.toLowerCase() + id + xpath;
    }
    return xpath;
}
JCD
Works perfectly as well! Thanks
Marc
line 7: never seen that syntax, usually it would be written like this: `id = id > 1 ? ('[' + id + ']') : '';`
frunsi
You're correct, I would have written it as you did, but I was just copying/pasting...I found this script some time ago and never bothered to clean it up. Either way, both ways of writing it are equivalent.
JCD
+1  A: 
function getPath(event) {
  event = event || window.event;

  var pathElements = [];
  var elem = event.currentTarget;
  var index = 0;
  var siblings = event.currentTarget.parentNode.getElementsByTagName(event.currentTarget.tagName);
  for (var i=0, imax=siblings.length; i<imax; i++) {
      if (event.currentTarget === siblings[i] {
        index = i+1; // add 1 for xpath 1-based
      }
  }


  while (elem.tagName.toLowerCase() != "html") {
    pathElements.unshift(elem.tagName);
    elem = elem.parentNode;
  }
  return pathElements.join("/") + "[" + index + "]";
}

EDITED TO ADD SIBLING INDEX INFORMATION

Robusto
Thanks for the suggestion, but this code doesn't seem to take into account similar sibling nodes. E.g. the code returns 'BODY/OL/LI' instead of 'BODY/OL/LI[2]'.
Marc
Picky, picky: I will edit and add functionality for that.
Robusto
Didn't mean to be picky, but the sibling index information is crucial for the problem I'm trying to solve. Anyway, thanks for updating your code!
Marc
+1  A: 

Just for fun, an XPath 2.0 one line implementation:

string-join(ancestor-or-self::*/concat(name(),
                                       '[',
                                       for $x in name() 
                                          return count(preceding-sibling::*
                                                          [name() = $x]) 
                                                 + 1,
                                       ']'),
            '/')
Alejandro
I'm not sure what it does, but it might come in handy at a later point. Thanks!
Marc