Get CSS path from Dom element
I got this function to get a cssPath :
var cssPath = function (el) {
var path = [];
while (
(el.nodeName.toLowerCase() != 'html') &&
(el = el.parentNode) &&
path.unshift(el.nodeName.toLowerCase() +
(el.id ? '#' + el.id : '') +
(el.className ? '.' + el.className.replace(/\s+/g, ".") : ''))
);
return path.join(" > ");
}
console.log(cssPath(document.getElementsByTagName('a')[123]));
But i got something like this :
html > body > div#div-id > div.site > div.clearfix > ul.choices > li
But to be totally right, it should look like this :
html > body > div#div-id > div.site:nth-child(1) > div.clearfix > ul.choices > li:nth-child(5)
Did someone have any idea to implement it simply in javascript ?
The answer above actually has a bug in it — the while loop breaks prematurely when it encounters a non-element node (e.g. a text node) resulting in an incorrect CSS selector.
Here's an improved version that fixes that problem plus:
- Stops when it encounters the first ancestor element with an id assigned to it
- Uses
nth-of-type()
to make the selectors more readable
var cssPath = function(el) { if (!(el instanceof Element)) return; var path = []; while (el.nodeType === Node.ELEMENT_NODE) { var selector = el.nodeName.toLowerCase(); if (el.id) { selector += '#' + el.id; path.unshift(selector); break; } else { var sib = el, nth = 1; while (sib = sib.previousElementSibling) { if (sib.nodeName.toLowerCase() == selector) nth++; } if (nth != 1) selector += ":nth-of-type("+nth+")"; } path.unshift(selector); el = el.parentNode; } return path.join(" > "); }
To always get the right element, you will need to use :nth-child()
or :nth-of-type()
for selectors that do not uniquely identify an element. So try this:
var cssPath = function(el) {
if (!(el instanceof Element)) return;
var path = [];
while (el.nodeType === Node.ELEMENT_NODE) {
var selector = el.nodeName.toLowerCase();
if (el.id) {
selector += '#' + el.id;
} else {
var sib = el, nth = 1;
while (sib.nodeType === Node.ELEMENT_NODE && (sib = sib.previousSibling) && nth++);
selector += ":nth-child("+nth+")";
}
path.unshift(selector);
el = el.parentNode;
}
return path.join(" > ");
}
You could add a routine to check for unique elements in their corresponding context (like TITLE
, BASE
, CAPTION
, etc.).
The two other provided answers had a couple of assumptions with browser compatibility that I ran into. Below code will not use nth-child and also has the previousElementSibling check.
function previousElementSibling (element) {
if (element.previousElementSibling !== 'undefined') {
return element.previousElementSibling;
} else {
// Loop through ignoring anything not an element
while (element = element.previousSibling) {
if (element.nodeType === 1) {
return element;
}
}
}
}
function getPath (element) {
// False on non-elements
if (!(element instanceof HTMLElement)) { return false; }
var path = [];
while (element.nodeType === Node.ELEMENT_NODE) {
var selector = element.nodeName;
if (element.id) { selector += ('#' + element.id); }
else {
// Walk backwards until there is no previous sibling
var sibling = element;
// Will hold nodeName to join for adjacent selection
var siblingSelectors = [];
while (sibling !== null && sibling.nodeType === Node.ELEMENT_NODE) {
siblingSelectors.unshift(sibling.nodeName);
sibling = previousElementSibling(sibling);
}
// :first-child does not apply to HTML
if (siblingSelectors[0] !== 'HTML') {
siblingSelectors[0] = siblingSelectors[0] + ':first-child';
}
selector = siblingSelectors.join(' + ');
}
path.unshift(selector);
element = element.parentNode;
}
return path.join(' > ');
}
Doing a reverse CSS selector lookup is an inherently tricky thing. I've generally come across two types of solutions:
Go up the DOM tree to assemble the selector string out of a combination of element names, classes, and the
id
orname
attribute. The problem with this method is that it can result in selectors that return multiple elements, which won't cut it if we require them to select only one unique element.Assemble the selector string using
nth-child()
ornth-of-type()
, which can result in very long selectors. In most cases the longer a selector is the higher specificity it has, and the higher the specificity the more likely it will break when the DOM structure changes.
The solution below is an attempt at tackling both of these issues. It is a hybrid approach that outputs a unique CSS selector (i.e., document.querySelectorAll(getUniqueSelector(el))
should always return a one-item array). While the returned selector string is not necessarily the shortest, it is derived with an eye towards CSS selector efficiency while balancing specificity by prioritizing nth-of-type()
and nth-child()
last.
You can specify what attributes to incorporate into the selector by updating the aAttr
array. The minimum browser requirement is IE 9.
function getUniqueSelector(elSrc) {
if (!(elSrc instanceof Element)) return;
var sSel,
aAttr = ['name', 'value', 'title', 'placeholder', 'data-*'], // Common attributes
aSel = [],
// Derive selector from element
getSelector = function(el) {
// 1. Check ID first
// NOTE: ID must be unique amongst all IDs in an HTML5 document.
// https://www.w3.org/TR/html5/dom.html#the-id-attribute
if (el.id) {
aSel.unshift('#' + el.id);
return true;
}
aSel.unshift(sSel = el.nodeName.toLowerCase());
// 2. Try to select by classes
if (el.className) {
aSel[0] = sSel += '.' + el.className.trim().replace(/ +/g, '.');
if (uniqueQuery()) return true;
}
// 3. Try to select by classes + attributes
for (var i=0; i<aAttr.length; ++i) {
if (aAttr[i]==='data-*') {
// Build array of data attributes
var aDataAttr = [].filter.call(el.attributes, function(attr) {
return attr.name.indexOf('data-')===0;
});
for (var j=0; j<aDataAttr.length; ++j) {
aSel[0] = sSel += '[' + aDataAttr[j].name + '="' + aDataAttr[j].value + '"]';
if (uniqueQuery()) return true;
}
} else if (el[aAttr[i]]) {
aSel[0] = sSel += '[' + aAttr[i] + '="' + el[aAttr[i]] + '"]';
if (uniqueQuery()) return true;
}
}
// 4. Try to select by nth-of-type() as a fallback for generic elements
var elChild = el,
sChild,
n = 1;
while (elChild = elChild.previousElementSibling) {
if (elChild.nodeName===el.nodeName) ++n;
}
aSel[0] = sSel += ':nth-of-type(' + n + ')';
if (uniqueQuery()) return true;
// 5. Try to select by nth-child() as a last resort
elChild = el;
n = 1;
while (elChild = elChild.previousElementSibling) ++n;
aSel[0] = sSel = sSel.replace(/:nth-of-type\(\d+\)/, n>1 ? ':nth-child(' + n + ')' : ':first-child');
if (uniqueQuery()) return true;
return false;
},
// Test query to see if it returns one element
uniqueQuery = function() {
return document.querySelectorAll(aSel.join('>')||null).length===1;
};
// Walk up the DOM tree to compile a unique selector
while (elSrc.parentNode) {
if (getSelector(elSrc)) return aSel.join(' > ');
elSrc = elSrc.parentNode;
}
}