50

In psuedo code, this is what I want.

var selector = $(this).cssSelectorAsString(); // Made up method...
// selector is now something like: "html>body>ul>li>img[3]"
var element = $(selector);

The reason is that I need to pass this off to an external environment, where a string is my only way to exchange data. This external environment then needs to send back a result, along with what element to update. So I need to be able to serialize a unique CSS selector for every element on the page.

I noticed jquery has a selector method, but it does not appear to work in this context. It only works if the object was created with a selector. It does not work if the object was created with an HTML node object.

2
  • Does the selector have to use the jQuery syntax (e.g. eq()), or can it be a general CSS selector as provided by many libraries? Commented Aug 26, 2015 at 5:10
  • A little off topic but I found this question trying to find a selector for many elements on a page, not just a particular one. This is what I came up with that only used tag and classes: element.parents().toArray().reverse().splice(2).map(e => e.localName + '.' + e.classList.value.replace(/ /g, '.')).join(' ') Commented May 7, 2017 at 17:02

10 Answers 10

57

I see now that a plugin existed (with the same name I thought of too), but here's just some quick JavaScript I wrote. It takes no consideration to the ids or classes of elements – only the structure (and adds :eq(x) where a node name is ambiguous).

jQuery.fn.getPath = function () {
    if (this.length != 1) throw 'Requires one element.';

    var path, node = this;
    while (node.length) {
        var realNode = node[0], name = realNode.name;
        if (!name) break;
        name = name.toLowerCase();

        var parent = node.parent();

        var siblings = parent.children(name);
        if (siblings.length > 1) { 
            name += ':eq(' + siblings.index(realNode) + ')';
        }

        path = name + (path ? '>' + path : '');
        node = parent;
    }

    return path;
};

(License: MIT)

Sign up to request clarification or add additional context in comments.

8 Comments

jQuery has a built-in index function that can take care of the loop part. Just say var i = siblings.index(node) and that ought to work.
@Dan: Ah, I had a feeling there'd be something like that, thanks =)
+1 Nice solution. I've made a solution working with multiple jQuery elements. But actually without your latest improvements. Maybe I will update it soon. See my answer...
I'm not sure if the solution to use an id if found is the better one. I know an id had to be unique, but I saw so many HTML code where the programmer didn't noticed that and used same id's multiole times. And I also know, that jQuery behaves different with not unique id's in different browsers. What do you think?
@algorhythm: that's why you should use a library: the problem is more complex than it seems. One such library explicitly checks that the id is indeed unique.
|
23

TL;DR - this is a more complex problem than it seems and you should use a library.


This problem appears easy at the first glance, but it's trickier than it seems, just as replacing plain URLs with links is non-trivial. Some considerations:

Further proof that the problem isn't as easy as it seems: there are 10+ libraries that generate CSS selectors, and the author of one of them has published this comparison.

Comments

10

jQuery-GetPath is a good starting point: it'll give you the item's ancestors, like this:

var path = $('#foo').getPath();
// e.g., "html > body > div#bar > ul#abc.def.ghi > li#foo"

2 Comments

jQuery-GetPath isn't on Github, and apparently hasn't been maintained since 2011. There are 10+ legit libraries that generate CSS selectors, and the author of one of them has published this comparison.
The most upvoted solution returned "undefined" for me, so this small function worked much better!
7

Here's a version of Blixt's answer that works in IE:

jQuery.fn.getPath = function () {
    if (this.length != 1) throw 'Requires one element.';

    var path, node = this;
    while (node.length) {
        var realNode = node[0];
        var name = (

            // IE9 and non-IE
            realNode.localName ||

            // IE <= 8
            realNode.tagName ||
            realNode.nodeName

        );

        // on IE8, nodeName is '#document' at the top level, but we don't need that
        if (!name || name == '#document') break;

        name = name.toLowerCase();
        if (realNode.id) {
            // As soon as an id is found, there's no need to specify more.
            return name + '#' + realNode.id + (path ? '>' + path : '');
        } else if (realNode.className) {
            name += '.' + realNode.className.split(/\s+/).join('.');
        }

        var parent = node.parent(), siblings = parent.children(name);
        if (siblings.length > 1) name += ':eq(' + siblings.index(node) + ')';
        path = name + (path ? '>' + path : '');

        node = parent;
    }

    return path;
};

1 Comment

This problem may seem simple, but in actuality it's a little more complex - generating unique CSS selectors that ideally are somewhat robust to changes in the page structure. There are 10+ libraries that generate CSS selectors, and the author of one of them has published this comparison.
5

I just wanted to share my version too because it is very clear to understand. I tested this script in all common browsers and it is working like a boss.

jQuery.fn.getPath = function () {
    var current = $(this);
    var path = new Array();
    var realpath = "BODY";
    while ($(current).prop("tagName") != "BODY") {
        var index = $(current).parent().find($(current).prop("tagName")).index($(current));
        var name = $(current).prop("tagName");
        var selector = " " + name + ":eq(" + index + ") ";
        path.push(selector);
        current = $(current).parent();
    }
    while (path.length != 0) {
        realpath += path.pop();
    }
    return realpath;
}

1 Comment

A selector that doesn't use ids is fragile.
4

Same solution like that one from @Blixt but compatible with multiple jQuery elements.

jQuery('.some-selector') can result in one or many DOM elements. @Blixt's solution works unfortunately only with the first one. My solution concatenates all them with ,.

If you want just handle the first element do it like this:

jQuery('.some-selector').first().getPath();

// or
jQuery('.some-selector:first').getPath();

Improved version

jQuery.fn.extend({
    getPath: function() {
        var pathes = [];

        this.each(function(index, element) {
            var path, $node = jQuery(element);

            while ($node.length) {
                var realNode = $node.get(0), name = realNode.localName;
                if (!name) { break; }

                name = name.toLowerCase();
                var parent = $node.parent();
                var sameTagSiblings = parent.children(name);

                if (sameTagSiblings.length > 1)
                {
                    allSiblings = parent.children();
                    var index = allSiblings.index(realNode) +1;
                    if (index > 0) {
                        name += ':nth-child(' + index + ')';
                    }
                }

                path = name + (path ? ' > ' + path : '');
                $node = parent;
            }

            pathes.push(path);
        });

        return pathes.join(',');
    }
});

Comments

2

If you are looking for a comprehensive, non-jQuery solution then you should try axe.utils.getSelector.

Comments

1

Following up on what alex wrote. jQuery-GetPath is a great starting point but I have modified it a little to incorporate :eq(), allowing me to distinguish between multiple id-less elements.

Add this before the getPath return line:

if (typeof id == 'undefined' && cur != 'body') {
    allSiblings = $(this).parent().children(cur);
    var index = allSiblings.index(this);// + 1;
    //if (index > 0) {
        cur += ':eq(' + index + ')';
    //}
}

This will return a path like "html > body > ul#hello > li.5:eq(1)"

2 Comments

There are 10+ libraries that generate CSS selectors, and the author of one of them has published this comparison.
@DanDascalescu The comparison hasn't been updated in a long time, unfortunately.
0

Update: This code was changed since then. You may find the implementation of the function now at css-login.js

Original answer: You may also have a look at findCssSelector, which is used in Firefox developer tools to save the currently selected node upon page refreshes. It doesn't use jQuery or any library.

const findCssSelector = function(ele) {
ele = getRootBindingParent(ele);
  let document = ele.ownerDocument;
  if (!document || !document.contains(ele)) {
    throw new Error("findCssSelector received element not inside document");
  }

  let cssEscape = ele.ownerGlobal.CSS.escape;

  // document.querySelectorAll("#id") returns multiple if elements share an ID
  if (ele.id &&
      document.querySelectorAll("#" + cssEscape(ele.id)).length === 1) {
    return "#" + cssEscape(ele.id);
  }

  // Inherently unique by tag name
  let tagName = ele.localName;
  if (tagName === "html") {
    return "html";
  }
  if (tagName === "head") {
    return "head";
  }
  if (tagName === "body") {
    return "body";
  }

  // We might be able to find a unique class name
  let selector, index, matches;
  if (ele.classList.length > 0) {
    for (let i = 0; i < ele.classList.length; i++) {
      // Is this className unique by itself?
      selector = "." + cssEscape(ele.classList.item(i));
      matches = document.querySelectorAll(selector);
      if (matches.length === 1) {
        return selector;
      }
      // Maybe it's unique with a tag name?
      selector = cssEscape(tagName) + selector;
      matches = document.querySelectorAll(selector);
      if (matches.length === 1) {
        return selector;
      }
      // Maybe it's unique using a tag name and nth-child
      index = positionInNodeList(ele, ele.parentNode.children) + 1;
      selector = selector + ":nth-child(" + index + ")";
      matches = document.querySelectorAll(selector);
      if (matches.length === 1) {
        return selector;
      }
    }
  }

  // Not unique enough yet.  As long as it's not a child of the document,
  // continue recursing up until it is unique enough.
  if (ele.parentNode !== document) {
    index = positionInNodeList(ele, ele.parentNode.children) + 1;
    selector = findCssSelector(ele.parentNode) + " > " +
      cssEscape(tagName) + ":nth-child(" + index + ")";
  }

  return selector;

};

2 Comments

getRootBindingParent(); what does it do?
@SantoshDangare This code was changed since then. You may find the implementation of the function now at hg.mozilla.org/mozilla-central/file/tip/devtools/shared/…
-1
$.fn.getSelector = function(){
    var $ele = $(this);
    return '#' + $ele.parents('[id!=""]').first().attr('id') 
               + ' .' + $ele.attr('class');
};

2 Comments

How is that going to help for solving the problem?
Oops, sorry, I should have mentioned this only works for selectors like '#foo .bar' which are very common in the world I live in.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.