Get CSS path from Dom element

Viewed 27998

I got this function to get a cssPath :

var cssPath = function (el) {
  var path = [];

  while (
    (el.nodeName.toLowerCase() != 'html') && 
    (el = el.parentNode) &&
    path.unshift(el.nodeName.toLowerCase() + 
      (el.id ? '#' + el.id : '') + 
      (el.className ? '.' + el.className.replace(/\s+/g, ".") : ''))
  );
  return path.join(" > ");
}
console.log(cssPath(document.getElementsByTagName('a')[123]));

But i got something like this :

html > body > div#div-id > div.site > div.clearfix > ul.choices > li

But to be totally right, it should look like this :

html > body > div#div-id > div.site:nth-child(1) > div.clearfix > ul.choices > li:nth-child(5)

Did someone have any idea to implement it simply in javascript ?

8 Answers

I somehow find all the implementations unreadable due to unnecessary mutation. Here I provide mine in ClojureScript and JS:

(defn element? [x]
  (and (not (nil? x))
      (identical? (.-nodeType x) js/Node.ELEMENT_NODE)))

(defn nth-child [el]
  (loop [sib el nth 1]
    (if sib
      (recur (.-previousSibling sib) (inc nth))
      (dec nth))))

(defn element-path
  ([el] (element-path el []))
  ([el path]
  (if (element? el)
    (let [tag (.. el -nodeName (toLowerCase))
          id (and (not (string/blank? (.-id el))) (.-id el))]
      (if id
        (element-path nil (conj path (str "#" id)))
        (element-path
          (.-parentNode el)
          (conj path (str tag ":nth-child(" (nth-child el) ")")))))
    (string/join " > " (reverse path)))))

Javascript:

const isElement = (x) => x && x.nodeType === Node.ELEMENT_NODE;

const nthChild = (el, nth = 1) => {
  if (el) {
    return nthChild(el.previousSibling, nth + 1);
  } else {
    return nth - 1;
  }
};

const elementPath = (el, path = []) => {
  if (isElement(el)) {
    const tag = el.nodeName.toLowerCase(),
          id = (el.id.length != 0 && el.id);
    if (id) {
      return elementPath(
        null, path.concat([`#${id}`]));
    } else {
      return elementPath(
        el.parentNode,
        path.concat([`${tag}:nth-child(${nthChild(el)})`]));
    }
  } else {
    return path.reverse().join(" > ");
  }
};
function cssPath (e, anchor) {
    var selector;

    var parent = e.parentNode, child = e;
    var tagSelector = e.nodeName.toLowerCase();

    while (anchor && parent != anchor || !anchor && parent.nodeType === NodeTypes.ELEMENT_NODE) {
        var cssAttributes = ['id', 'name', 'class', 'type', 'alt', 'title', 'value'];
        var childSelector = tagSelector;
        if (!selector || parent.querySelectorAll (selector).length > 1) {
            for (var i = 0; i < cssAttributes.length; i++) {
                var attr = cssAttributes[i];
                var value = child.getAttribute(attr);
                if (value) {
                    if (attr === 'id') {
                        childSelector = '#' + value;
                    } else if (attr === 'class') {
                        childSelector = childSelector + '.' + value.replace(/\s/g, ".").replace(/\.\./g, ".");
                    } else { 
                        childSelector = childSelector + '[' + attr + '="' + value + '"]';
                    }
                }
            }

            var putativeSelector = selector? childSelector + ' ' + selector: childSelector;             

            if (parent.querySelectorAll (putativeSelector).length > 1) {
                var siblings = parent.querySelectorAll (':scope > ' + tagSelector);
                for (var index = 0; index < siblings.length; index++)
                    if (siblings [index] === child) {
                        childSelector = childSelector + ':nth-of-type(' + (index + 1) + ')';
                        putativeSelector = selector? childSelector + ' ' + selector: childSelector;             
                        break;
                    }
            }

            selector = putativeSelector;
        }
        child = parent;
        parent = parent.parentNode;
    }

    return selector;
};      

Better late than never: I came to this question and tried to use the selected answer, but in my case, it didn't worked because it wasn't very specific for my case. So I decided to write my own solution - I hope it may help some.

This solution goes like this: tag.class#id[name][type]:nth-child(?), and targeted with >.

function path(e) {
    let a = [];
    while (e.parentNode) {
        let d = [
            e.tagName.toLowerCase(),
            e.hasAttribute("class") ? e.getAttribute("class") : "",
            e.hasAttribute("id") ? e.getAttribute("id") : "",
            e.hasAttribute("name") ? e.getAttribute("name") : "",
            e.hasAttribute("type") ? e.getAttribute("type") : "",       
            0                                                       // nth-child
        ];

        // Trim
        for (let i = 0; i < d.length; i++) d[i] = typeof d[i] == "string" ? d[i].trim() : d[i];

        if (d[1] != "") d[1] = "."+d[1].split(" ").join(".");
        if (d[2] != "") d[2] = "#"+d[2];
        if (d[3] != "") d[3] = '[name="'+d[3]+'"]';
        if (d[4] != "") d[4] = '[type="'+d[4]+'"]';
        // Get child index...
        let s = e;
        while (s) {
            d[5]++;
            s = s.previousElementSibling;
        }
        d[5] = d[5] != "" ? ":nth-child("+d[5]+")" : ":only-child";
        // Build the String
        s = "";
        for (let i = 0; i < d.length; i++) s += d[i];
        a.unshift(s);

        // Go to Parent
        e = e.parentNode;
    }
    return a.join(">");
}

I know it's not that readable (I use it in my messy code), but it will give you the exact element(s) you're looking for. Just try it.

Related