2

I am trying to find a way to build the querySelector string of any given node. In other words - pick any node on a page - is it possible to walk up the DOM and build a string that would allow me to pass the generated string to document.querySelector and get back the node I chose?

From what I can tell querySelector has a bug where you can only use nth-child once in the string.

I have tried several times but so far have failed to find a solution. I want to do this in native JavaScript, not jQuery.Any suggestions?

BoltClock
  • 700,868
  • 160
  • 1,392
  • 1,356
Steve Lloyd
  • 773
  • 2
  • 12
  • 33
  • 1
    "From what I can tell querySelector has a bug where you can only use nth-child once in the string." I'm curious, where did you read this? Can you show us what you've tried? – BoltClock Apr 22 '15 at 04:50
  • 2
    If the element doesn't have an id already, then generate a unique ID for it, put the id on the object and then use that with `document.querySelector()`. What problem are you really trying to solve? You can always just save an actual reference to the DOM element itself if you just want to be able to get back to it. – jfriend00 Apr 22 '15 at 04:52
  • And how about assigning an id to that node and then just using qerySelector("#id")? – Christophe Apr 22 '15 at 04:53
  • 1
    You can use xpath based node selection. `document.evaluate` in Chrome & FF and `document.selectNodes` in IE – Vijay Apr 22 '15 at 04:55
  • OK apparently I once commented on this question from a few years ago: http://stackoverflow.com/questions/6345358/queryselector-with-nested-nth-child-in-chrome-doesnt-appear-to-work Apparently different browsers have different bugs with nth-child in querySelector. I'm not sure if the situation has improved for either browser though. – BoltClock Apr 22 '15 at 04:56
  • @Vijay: That's all fine and dandy but how would you build the XPath required to get the element then? – BoltClock Apr 22 '15 at 05:00
  • 2
    @BoltClock: https://developer.mozilla.org/en/docs/Using_XPath#getXPathForElement – Amadan Apr 22 '15 at 05:03
  • @Amadan: Great, if only Vijay mentioned it, because without it, his comment is quite useless. – BoltClock Apr 22 '15 at 05:05
  • @BoltClock it's a useful comment, just not meant to be an answer. – Christophe Apr 22 '15 at 05:10
  • @Christophe: How is "you can use XPath" without stating how to build said XPath a useful comment on a question asking how to build a path to an element? At best, it doesn't bring OP any closer at all to a solution; at worst, it's utterly irrelevant (and I am very much aware of XPath and selectors being two of the most common tools for locating elements). – BoltClock Apr 22 '15 at 05:21
  • 1
    I am unable to modify the DOM so adding a unique ID is out of the question. I looked at the xpath link at https://developer.mozilla.org/en/docs/Using_XPath#getXPathForElement but I am unsure what xml document I am passing to the function. As far the multiple nth-child in a querySelector call I tried it myself and it still fails. I also read it at http://stackoverflow.com/questions/6345358/queryselector-with-nested-nth-child-in-chrome-doesnt-appear-to-work and https://github.com/ariya/phantomjs/issues/11632 – Steve Lloyd Apr 22 '15 at 11:48
  • 2
    @Steve Lloyd: I *think* you can simply pass `document` directly as the second argument to getXPathForElement. – BoltClock Apr 22 '15 at 18:10
  • New link: https://developer.mozilla.org/en-US/docs/Web/XPath/Snippets#getxpathforelement – coderfin Sep 22 '22 at 07:12

2 Answers2

6

I see this question is from 2015 but I just dealt with this issue and had to build a custom function to do so.

I've made a snippet to test it, just click any element and the querySelector should display as a string in the bottom div.

function getQuerySelector(elem) {

  var element = elem;
  var str = "";

  function loop(element) {

    // stop here = element has ID
    if(element.getAttribute("id")) {
      str = str.replace(/^/, " #" + element.getAttribute("id"));
      str = str.replace(/\s/, "");
      str = str.replace(/\s/g, " > ");
      return str;
    }

    // stop here = element is body
    if(document.body === element) {
      str = str.replace(/^/, " body");
      str = str.replace(/\s/, "");
      str = str.replace(/\s/g, " > ");
      return str;
    }

    // concat all classes in "queryselector" style
    if(element.getAttribute("class")) {
      var elemClasses = ".";
      elemClasses += element.getAttribute("class");
      elemClasses = elemClasses.replace(/\s/g, ".");
      elemClasses = elemClasses.replace(/^/g, " ");
      var classNth = "";

      // check if element class is the unique child
      var childrens = element.parentNode.children;

      if(childrens.length < 2) {
        return;
      }

      var similarClasses = [];

      for(var i = 0; i < childrens.length; i++) {
        if(element.getAttribute("class") == 
childrens[i].getAttribute("class")) {
          similarClasses.push(childrens[i]);
        }
      }

      if(similarClasses.length > 1) {
        for(var j = 0; j < similarClasses.length; j++) {
          if(element === similarClasses[j]) {
            j++;
            classNth = ":nth-of-type(" + j + ")";
            break;
          }
        }
      }

      str = str.replace(/^/, elemClasses + classNth);

    }
    else{

      // get nodeType
      var name = element.nodeName;
      name = name.toLowerCase();
      var nodeNth = "";

      var childrens = element.parentNode.children;

      if(childrens.length > 2) {
        var similarNodes = [];

        for(var i = 0; i < childrens.length; i++) {
          if(element.nodeName == childrens[i].nodeName) {
            similarNodes.push(childrens[i]);
          }
        }

        if(similarNodes.length > 1) {
          for(var j = 0; j < similarNodes.length; j++) {
            if(element === similarNodes[j]) {
              j++;
              nodeNth = ":nth-of-type(" + j + ")";
              break;
            }
          }
        }

      }

      str = str.replace(/^/, " " + name + nodeNth);

    }

    if(element.parentNode) {
      loop(element.parentNode);
    }
    else {
      str = str.replace(/\s/g, " > ");
      str = str.replace(/\s/, "");
      return str;
    }

  }

  loop(element);

  return str;


}

https://jsfiddle.net/wm6goeyw/

dvd
  • 179
  • 3
  • 11
  • This is a pretty clever solution. I did notice; however, that it breaks when you have a single element with a class inside another element with a class (`if(childrens.length < 2)`) A quick fix that could probably be improved on is: `if(childrens.length < 2) { str = str.replace(/^/, elemClasses); if(element.parentNode) { loop(element.parentNode); } return;} ` – coderfin Sep 22 '22 at 07:07
0

This works as long as DOM structure remains same. Even little change in HTML code could result to incorrect (in terms of return value) result of this function. So, it is not intended for long storing of DOM reference to HTML element.

function createQuerySelector(element) {
    if (element.id) {
        return `#${element.id}`;
    }

    const path = [];
    let currentElement = element;
    let error = false;

    while (currentElement.tagName !== 'BODY') {
        const parent = currentElement.parentElement;

        if (!parent) {
            error = true;
            break;
        }

        const childTagCount= {};
        let nthChildFound = false;

        for (const child of parent.children) {
            const tag = child.tagName;
            const count = childTagCount[tag] || 0;
            childTagCount[tag] = count + 1;

            if (child === currentElement) {
                nthChildFound = true;
                break;
            }
        }

        if (!nthChildFound) {
            error = true;
            break;
        }

        const count = childTagCount[currentElement.tagName];
        const tag = currentElement.tagName.toLowerCase();
        const selector = `${tag}:nth-of-type(${count})`;

        path.push(selector);
        currentElement = parent;
    }

    if (error) {
        console.error(element);
        throw new Error('Unable to create query selector');
    }

    path.push('body');

    const querySelector = path.reverse().join(' > ');

    return querySelector;
}
Amaimersion
  • 787
  • 15
  • 28