How to calculate the XPath position of an element using Javascript?
Solution 1
Firebug can do this, and it's open source (BSD) so you can reuse their implementation, which does not require any libraries.
3rd party edit
This is an extract from the linked source above. Just in case the link above will change. Please check the source to benefit from changes and updates or the full featureset provided.
Xpath.getElementXPath = function(element)
{
if (element && element.id)
return '//*[@id="' + element.id + '"]';
else
return Xpath.getElementTreeXPath(element);
};
Above code calls this function. Attention i added some line-wrapping to avoid horizontal scroll bar
Xpath.getElementTreeXPath = function(element)
{
var paths = []; // Use nodeName (instead of localName)
// so namespace prefix is included (if any).
for (; element && element.nodeType == Node.ELEMENT_NODE;
element = element.parentNode)
{
var index = 0;
var hasFollowingSiblings = false;
for (var sibling = element.previousSibling; sibling;
sibling = sibling.previousSibling)
{
// Ignore document type declaration.
if (sibling.nodeType == Node.DOCUMENT_TYPE_NODE)
continue;
if (sibling.nodeName == element.nodeName)
++index;
}
for (var sibling = element.nextSibling;
sibling && !hasFollowingSiblings;
sibling = sibling.nextSibling)
{
if (sibling.nodeName == element.nodeName)
hasFollowingSiblings = true;
}
var tagName = (element.prefix ? element.prefix + ":" : "")
+ element.localName;
var pathIndex = (index || hasFollowingSiblings ? "["
+ (index + 1) + "]" : "");
paths.splice(0, 0, tagName + pathIndex);
}
return paths.length ? "/" + paths.join("/") : null;
};
Solution 2
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;
}
Solution 3
Small, powerfull and pure-js function
It returns xpath for the element and elements iterator for xpath.
https://gist.github.com/iimos/e9e96f036a3c174d0bf4
function xpath(el) {
if (typeof el == "string") return document.evaluate(el, document, null, 0, null)
if (!el || el.nodeType != 1) return ''
if (el.id) return "//*[@id='" + el.id + "']"
var sames = [].filter.call(el.parentNode.children, function (x) { return x.tagName == el.tagName })
return xpath(el.parentNode) + '/' + el.tagName.toLowerCase() + (sames.length > 1 ? '['+([].indexOf.call(sames, el)+1)+']' : '')
}
Probably you will need to add a shim for IE8 that don't support the [].filter method: this MDN page gives such code.
Usage
Getting xpath for node:var xp = xpath(elementNode)
Executing xpath:
var iterator = xpath("//h2")
var el = iterator.iterateNext();
while (el) {
// work with element
el = iterator.iterateNext();
}
Solution 4
The firebug implementation can be modified slightly to check for element.id further up the dom tree:
/**
* Gets an XPath for an element which describes its hierarchical location.
*/
var getElementXPath = function(element) {
if (element && element.id)
return '//*[@id="' + element.id + '"]';
else
return getElementTreeXPath(element);
};
var getElementTreeXPath = function(element) {
var paths = [];
// Use nodeName (instead of localName) so namespace prefix is included (if any).
for (; element && element.nodeType == 1; element = element.parentNode) {
var index = 0;
// EXTRA TEST FOR ELEMENT.ID
if (element && element.id) {
paths.splice(0, 0, '/*[@id="' + element.id + '"]');
break;
}
for (var sibling = element.previousSibling; sibling; sibling = sibling.previousSibling) {
// Ignore document type declaration.
if (sibling.nodeType == Node.DOCUMENT_TYPE_NODE)
continue;
if (sibling.nodeName == element.nodeName)
++index;
}
var tagName = element.nodeName.toLowerCase();
var pathIndex = (index ? "[" + (index+1) + "]" : "");
paths.splice(0, 0, tagName + pathIndex);
}
return paths.length ? "/" + paths.join("/") : null;
};
Solution 5
I have just modified DanS' solution in order to use it with textNodes. Very useful to serialize HTML range object.
/**
* Gets an XPath for an node which describes its hierarchical location.
*/
var getNodeXPath = function(node) {
if (node && node.id)
return '//*[@id="' + node.id + '"]';
else
return getNodeTreeXPath(node);
};
var getNodeTreeXPath = function(node) {
var paths = [];
// Use nodeName (instead of localName) so namespace prefix is included (if any).
for (; node && (node.nodeType == 1 || node.nodeType == 3) ; node = node.parentNode) {
var index = 0;
// EXTRA TEST FOR ELEMENT.ID
if (node && node.id) {
paths.splice(0, 0, '/*[@id="' + node.id + '"]');
break;
}
for (var sibling = node.previousSibling; sibling; sibling = sibling.previousSibling) {
// Ignore document type declaration.
if (sibling.nodeType == Node.DOCUMENT_TYPE_NODE)
continue;
if (sibling.nodeName == node.nodeName)
++index;
}
var tagName = (node.nodeType == 1 ? node.nodeName.toLowerCase() : "text()");
var pathIndex = (index ? "[" + (index+1) + "]" : "");
paths.splice(0, 0, tagName + pathIndex);
}
return paths.length ? "/" + paths.join("/") : null;
};
Related videos on Youtube
Comments
-
Marc about 2 years
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: generate/get the Xpath for a selected textnode
-
Matthew Flaschen almost 14 yearsXPath uses 1-based indexing, so it's
li[2]
. -
Marc almost 14 yearsThanks, I've changed the code.
-
-
Marc almost 14 yearsThanks 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 almost 14 yearsWorks perfectly as well! Thanks
-
Marc almost 14 yearsI'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!
-
Frunsi almost 14 yearsline 7: never seen that syntax, usually it would be written like this:
id = id > 1 ? ('[' + id + ']') : '';
-
JCD almost 14 yearsYou'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.
-
Marc almost 14 yearsI'm not sure what it does, but it might come in handy at a later point. Thanks!
-
Marc almost 14 yearsDidn'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!
-
OneWorld about 12 yearsI like the Firebug implementation (getElementXPath), because it favors the ID over a xpath tree if the element has any.
-
DanS almost 12 years@OneWorld Yes, but it only checks the element you are getting the xpath of and not it's parents. See my answer, it produces an xpath more like the google chrome inspector.
-
Sali Hoo over 10 yearsDo the following code returns correct xpath for the div in following html?
-
wadim almost 10 yearsFirebug's implementation has a bug in line 1365: it only adds the index, (e.g. "[3]") if there are previous siblings of same type. This is wrong because an XPath without the index will match all siblings of this type, even further ones. Example:
/p/b
will match allb
tags under allp
tags under root. Firebug just skips the index if it doesn't find previous siblings of same type. -
wadim almost 10 yearsThis code has several problems: (1) when counting sibling index it actually checks if any of the siblings is the target of the event (instead of checking if any of the siblings is the same node type as the event target), (2) it takes into account sibling index information on the deepest level only, not further up the tree.
-
iimos over 8 yearsBUG: On first tag of
<div/><div/>
it returns '/div' instead of '/div[1]' -
Yoiku over 8 yearsWhich was the file that contain this method? I need somthing similar but firebug was move to github and I can't find the method
-
qqilihq almost 8 yearsOn current versions, the mentioned implementation can be found here: github.com/firebug/firebug/blob/master/extension/content/…
-
DaveRead over 4 yearsExample javascript/jQuery link is no longer available
-
Nathan over 2 yearsThis doesn't work for me at all. I'm passing in a startContainer node from a Range object and am getting null.