Getting a jQuery selector for an element

JavascriptJqueryJquery Selectors

Javascript Problem Overview


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.

Javascript Solutions


Solution 1 - Javascript

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)

Solution 2 - Javascript

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.

Solution 3 - Javascript

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"

Solution 4 - Javascript

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;
};

Solution 5 - Javascript

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;
}

Solution 6 - Javascript

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(',');
	}
});

Solution 7 - Javascript

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

Solution 8 - Javascript

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)"

Solution 9 - Javascript

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;

};

Solution 10 - Javascript

$.fn.getSelector = function(){
    var $ele = $(this);
    return '#' + $ele.parents('[id!=""]').first().attr('id') 
               + ' .' + $ele.attr('class');
};

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAlex WayneView Question on Stackoverflow
Solution 1 - JavascriptBlixtView Answer on Stackoverflow
Solution 2 - JavascriptDan DascalescuView Answer on Stackoverflow
Solution 3 - JavascriptAgosView Answer on Stackoverflow
Solution 4 - JavascriptcrizCraigView Answer on Stackoverflow
Solution 5 - JavascriptAhmet Can GüvenView Answer on Stackoverflow
Solution 6 - JavascriptalgorhythmView Answer on Stackoverflow
Solution 7 - JavascriptKonrad DzwinelView Answer on Stackoverflow
Solution 8 - JavascriptDevelop IdeasView Answer on Stackoverflow
Solution 9 - JavascriptAshraf SabryView Answer on Stackoverflow
Solution 10 - Javascriptpepper69View Answer on Stackoverflow