A JavaScript reverse CSS selector

In plain Javascript, you can get a reference to a particular HTML element by using the querySelector method. For example:

document.querySelector('div')

Will give you a reference to the first “div” that’s encountered in the page. Give it a try in DevTools

Sample querySelector

What if we want to do the reverse? That is, given an element, get the correct query selector. In essence this is what Chrome DevTools does when you right click on an element and “Copy Selector”

Getting the Selector given an element

Seems to me that we can go through the process of writing our own function and testing all the edge cases – but if Chrome does it, maybe we could reverse engineer the code and use that…

The code we’re interested in resides here, in the open source fork of Chrome (Chromium):

https://chromium.googlesource.com/chromium/blink/+/master/Source/devtools/front_end/components/DOMPresentationUtils.js

After an hour or so of figuring out which parts we need, and which parts we broke, we end up with the code attached to the end of this article. Let’s test…. first a quick “click” listener which prints out the results whenever we click an HTML element:

document.addEventListener('click', function(e) { 
    console.log(TopLevelObject.DOMPresentationUtils.cssPath(e.target))
})
Test screencast

As I click randomly in the page on the left, note how the selectors appear in the console window on the bottom right

var TopLevelObject = {}
TopLevelObject.DOMNodePathStep = function(value, optimized)
{
this.value = value;
this.optimized = optimized || false;
}
TopLevelObject.DOMNodePathStep.prototype = {
/**
* @override
* @return {string}
*/
toString: function()
{
return this.value;
}
}
TopLevelObject.DOMPresentationUtils = {}
TopLevelObject.DOMPresentationUtils.cssPath = function(node, optimized)
{
if (node.nodeType !== Node.ELEMENT_NODE)
return "";
var steps = [];
var contextNode = node;
while (contextNode) {
var step = TopLevelObject.DOMPresentationUtils._cssPathStep(contextNode, !!optimized, contextNode === node);
if (!step)
break; // Error – bail out early.
steps.push(step);
if (step.optimized)
break;
contextNode = contextNode.parentNode;
}
steps.reverse();
return steps.join(" > ");
}
TopLevelObject.DOMPresentationUtils._cssPathStep = function(node, optimized, isTargetNode)
{
if (node.nodeType !== Node.ELEMENT_NODE)
return null;
var id = node.getAttribute("id");
if (optimized) {
if (id)
return new TopLevelObject.DOMNodePathStep(idSelector(id), true);
var nodeNameLower = node.nodeName.toLowerCase();
if (nodeNameLower === "body" || nodeNameLower === "head" || nodeNameLower === "html")
return new TopLevelObject.DOMNodePathStep(node.tagName.toLowerCase(), true);
}
var nodeName = node.tagName.toLowerCase();
if (id)
return new TopLevelObject.DOMNodePathStep(nodeName + idSelector(id), true);
var parent = node.parentNode;
if (!parent || parent.nodeType === Node.DOCUMENT_NODE)
return new TopLevelObject.DOMNodePathStep(nodeName, true);
/**
* @param {!TopLevelObject.DOMNode} node
* @return {!Array.<string>}
*/
function prefixedElementClassNames(node)
{
var classAttribute = node.getAttribute("class");
if (!classAttribute)
return [];
return classAttribute.split(/\s+/g).filter(Boolean).map(function(name) {
// The prefix is required to store "__proto__" in a object-based map.
return "$" + name;
});
}
/**
* @param {string} id
* @return {string}
*/
function idSelector(id)
{
return "#" + escapeIdentifierIfNeeded(id);
}
/**
* @param {string} ident
* @return {string}
*/
function escapeIdentifierIfNeeded(ident)
{
if (isCSSIdentifier(ident))
return ident;
var shouldEscapeFirst = /^(?:[0-9]|[0-9-]?)/.test(ident);
var lastIndex = ident.length 1;
return ident.replace(/./g, function(c, i) {
return ((shouldEscapeFirst && i === 0) || !isCSSIdentChar(c)) ? escapeAsciiChar(c, i === lastIndex) : c;
});
}
/**
* @param {string} c
* @param {boolean} isLast
* @return {string}
*/
function escapeAsciiChar(c, isLast)
{
return "\\" + toHexByte(c) + (isLast ? "" : " ");
}
/**
* @param {string} c
*/
function toHexByte(c)
{
var hexByte = c.charCodeAt(0).toString(16);
if (hexByte.length === 1)
hexByte = "0" + hexByte;
return hexByte;
}
/**
* @param {string} c
* @return {boolean}
*/
function isCSSIdentChar(c)
{
if (/[a-zA-Z0-9_-]/.test(c))
return true;
return c.charCodeAt(0) >= 0xA0;
}
/**
* @param {string} value
* @return {boolean}
*/
function isCSSIdentifier(value)
{
return /^-?[a-zA-Z_][a-zA-Z0-9_-]*$/.test(value);
}
var prefixedOwnClassNamesArray = prefixedElementClassNames(node);
var needsClassNames = false;
var needsNthChild = false;
var ownIndex = 1;
var elementIndex = 1;
var siblings = parent.children;
for (var i = 0; (ownIndex === 1 || !needsNthChild) && i < siblings.length; ++i) {
var sibling = siblings[i];
if (sibling.nodeType !== Node.ELEMENT_NODE)
continue;
elementIndex += 1;
if (sibling === node) {
ownIndex = elementIndex;
continue;
}
if (needsNthChild)
continue;
if (sibling.tagName.toLowerCase() !== nodeName)
continue;
needsClassNames = true;
var ownClassNames = prefixedOwnClassNamesArray.values();
var ownClassNameCount = 0;
for (var name in ownClassNames)
++ownClassNameCount;
if (ownClassNameCount === 0) {
needsNthChild = true;
continue;
}
var siblingClassNamesArray = prefixedElementClassNames(sibling);
for (var j = 0; j < siblingClassNamesArray.length; ++j) {
var siblingClass = siblingClassNamesArray[j];
if (!ownClassNames.hasOwnProperty(siblingClass))
continue;
delete ownClassNames[siblingClass];
if (!ownClassNameCount) {
needsNthChild = true;
break;
}
}
}
var result = nodeName;
if (isTargetNode && nodeName.toLowerCase() === "input" && node.getAttribute("type") && !node.getAttribute("id") && !node.getAttribute("class"))
result += "[type=\"" + node.getAttribute("type") + "\"]";
if (needsNthChild) {
result += ":nth-child(" + (ownIndex + 1) + ")";
} else if (needsClassNames) {
for (var prefixedName in prefixedOwnClassNamesArray.values())
result += "." + escapeIdentifierIfNeeded(prefixedName.substr(1));
}
return new TopLevelObject.DOMNodePathStep(result, false);
}