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.

BoltClock
  • 700,868
  • 160
  • 1,392
  • 1,356
Alex Wayne
  • 178,991
  • 47
  • 309
  • 337
  • 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](http://stackoverflow.com/questions/2068272/getting-a-jquery-selector-for-an-element#32218234)? – Dan Dascalescu Aug 26 '15 at 05: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(' ')` – Jason Goemaat May 07 '17 at 17:02

10 Answers10

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)

Blixt
  • 49,547
  • 13
  • 120
  • 153
  • 1
    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 Jan 15 '10 at 00:00
  • @Dan: Ah, I had a feeling there'd be something like that, thanks =) – Blixt Jan 15 '10 at 00:06
  • +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... – algorhythm Nov 05 '14 at 17:29
  • 2
    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 Nov 05 '14 at 17:51
  • @algorhythm: that's why you should [use a library](http://stackoverflow.com/questions/2068272/getting-a-jquery-selector-for-an-element/32218234#32218234): the problem is more complex than it seems. One such library explicitly [checks that the id is indeed unique](https://github.com/fczbkk/css-selector-generator/commit/0f069db62098dd7cbe74be9afbcd27154abde550). – Dan Dascalescu Aug 26 '15 at 08:31
  • I would stay away from `localName` since it's now [obsolete](https://developer.mozilla.org/en-US/docs/Web/API/Node/localName). Use `nodeName` (or `nodeName.toLowerCase()` if you have to) instead. – thdoan Jun 01 '17 at 02:07
  • Nothing like commenting on a 10-year old answer... but... Any chance you can declare a license for this block of code? By default, SO answers prior to 2016 carry a "CC BY-SA 4.0" license, but that's ambiguously defined (and not recommended) for code due to distribution issues. I'm hoping you can tag this as something like "Apache 2.0" or "MIT with attribution" (current SO standard). Many thanks – Jeff Bennett Jan 12 '21 at 11:20
  • Absolutely! MIT without attribution is all fine by me. Attribution or link back to this post could be nice but not a requirement. Is there any place I can more officially set the license, since you mentioned SO has a default before/after 2016? – Blixt Jan 12 '21 at 13:31
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.

Community
  • 1
  • 1
Dan Dascalescu
  • 143,271
  • 52
  • 317
  • 404
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"
alex
  • 479,566
  • 201
  • 878
  • 984
Agos
  • 18,542
  • 11
  • 56
  • 70
  • 3
    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](https://github.com/fczbkk/css-selector-generator-benchmark). – Dan Dascalescu Aug 26 '15 at 04:39
  • 1
    The most upvoted solution returned "undefined" for me, so this small function worked much better! – Alvaro Jul 14 '20 at 11:42
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;
};
crizCraig
  • 8,487
  • 6
  • 54
  • 53
  • 1
    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](https://github.com/fczbkk/css-selector-generator-benchmark). – Dan Dascalescu Aug 26 '15 at 04:40
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;
}
Ahmet Can Güven
  • 5,392
  • 4
  • 38
  • 59
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(',');
    }
});
algorhythm
  • 8,530
  • 3
  • 35
  • 47
2

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

Konrad Dzwinel
  • 36,825
  • 12
  • 98
  • 105
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)"

Develop Ideas
  • 103
  • 1
  • 1
  • 9
  • There are 10+ libraries that generate CSS selectors, and the author of one of them has published [this comparison](https://github.com/fczbkk/css-selector-generator-benchmark). – Dan Dascalescu Aug 26 '15 at 04:42
  • @DanDascalescu The comparison hasn't been updated in a long time, unfortunately. – James Moore Aug 16 '17 at 03:14
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;

};
Ashraf Sabry
  • 3,081
  • 3
  • 32
  • 29
-1
$.fn.getSelector = function(){
    var $ele = $(this);
    return '#' + $ele.parents('[id!=""]').first().attr('id') 
               + ' .' + $ele.attr('class');
};
pepper69
  • 1
  • 2