javascript 从 Dom 元素获取 CSS 路径

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/3620116/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-10-25 01:45:34  来源:igfitidea点击:

Get CSS path from Dom element

javascriptcsscss-selectors

提问by jney

I got this function to get a cssPath :

我得到了这个函数来获取 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 ?

有人有什么想法可以简单地在 javascript 中实现它吗?

采纳答案by Gumbo

To always get the right element, you will need to use :nth-child()or :nth-of-type()for selectors that do not uniquely identify an element. So try this:

要始终获得正确的元素,您将需要使用:nth-child():nth-of-type()用于不能唯一标识元素的选择器。所以试试这个:

var cssPath = function(el) {
    if (!(el instanceof Element)) return;
    var path = [];
    while (el.nodeType === Node.ELEMENT_NODE) {
        var selector = el.nodeName.toLowerCase();
        if (el.id) {
            selector += '#' + el.id;
        } else {
            var sib = el, nth = 1;
            while (sib.nodeType === Node.ELEMENT_NODE && (sib = sib.previousSibling) && nth++);
            selector += ":nth-child("+nth+")";
        }
        path.unshift(selector);
        el = el.parentNode;
    }
    return path.join(" > ");
}

You could add a routine to check for unique elements in their corresponding context (like TITLE, BASE, CAPTION, etc.).

你可以添加一个例行检查在其对应的背景下独特的元素(如TITLEBASECAPTION,等)。

回答by asselin

The answer above actually has a bug in it — the while loop breaks prematurely when it encounters a non-element node (e.g. a text node) resulting in an incorrect CSS selector.

上面的答案实际上有一个错误——while 循环在遇到非元素节点(例如文本节点)时会过早中断,从而导致 CSS 选择器不正确。

Here's an improved version that fixes that problem plus:

这是修复该问题的改进版本以及:

  • Stops when it encounters the first ancestor element with an id assigned to it
  • Uses nth-of-type()to make the selectors more readable
  • 当遇到第一个分配有 id 的祖先元素时停止
  • 用于nth-of-type()使选择器更具可读性
    var cssPath = function(el) {
        if (!(el instanceof Element)) 
            return;
        var path = [];
        while (el.nodeType === Node.ELEMENT_NODE) {
            var selector = el.nodeName.toLowerCase();
            if (el.id) {
                selector += '#' + el.id;
                path.unshift(selector);
                break;
            } else {
                var sib = el, nth = 1;
                while (sib = sib.previousElementSibling) {
                    if (sib.nodeName.toLowerCase() == selector)
                       nth++;
                }
                if (nth != 1)
                    selector += ":nth-of-type("+nth+")";
            }
            path.unshift(selector);
            el = el.parentNode;
        }
        return path.join(" > ");
     }

回答by BenM

The two other provided answers had a couple of assumptions with browser compatibility that I ran into. Below code will not use nth-child and also has the previousElementSibling check.

另外两个提供的答案对我遇到的浏览器兼容性有几个假设。下面的代码不会使用 nth-child 并且还有 previousElementSibling 检查。

function previousElementSibling (element) {
  if (element.previousElementSibling !== 'undefined') {
    return element.previousElementSibling;
  } else {
    // Loop through ignoring anything not an element
    while (element = element.previousSibling) {
      if (element.nodeType === 1) {
        return element;
      }
    }
  }
}
function getPath (element) {
  // False on non-elements
  if (!(element instanceof HTMLElement)) { return false; }
  var path = [];
  while (element.nodeType === Node.ELEMENT_NODE) {
    var selector = element.nodeName;
    if (element.id) { selector += ('#' + element.id); }
    else {
      // Walk backwards until there is no previous sibling
      var sibling = element;
      // Will hold nodeName to join for adjacent selection
      var siblingSelectors = [];
      while (sibling !== null && sibling.nodeType === Node.ELEMENT_NODE) {
        siblingSelectors.unshift(sibling.nodeName);
        sibling = previousElementSibling(sibling);
      }
      // :first-child does not apply to HTML
      if (siblingSelectors[0] !== 'HTML') {
        siblingSelectors[0] = siblingSelectors[0] + ':first-child';
      }
      selector = siblingSelectors.join(' + ');
    }
    path.unshift(selector);
    element = element.parentNode;
  }
  return path.join(' > ');
}

回答by thdoan

Doing a reverse CSS selector lookup is an inherently tricky thing. I've generally come across two types of solutions:

执行反向 CSS 选择器查找本质上是一件棘手的事情。我通常遇到两种类型的解决方案:

  1. Go up the DOM tree to assemble the selector string out of a combination of element names, classes, and the idor nameattribute. The problem with this method is that it can result in selectors that return multiple elements, which won't cut it if we require them to select only one unique element.

  2. Assemble the selector string using nth-child()or nth-of-type(), which can result in very long selectors. In most cases the longer a selector is the higher specificity it has, and the higher the specificity the more likely it will break when the DOM structure changes.

  1. 沿着 DOM 树向上走,从元素名称、类和idorname属性的组合中组合出选择器字符串。这种方法的问题在于它可能导致选择器返回多个元素,如果我们要求它们只选择一个唯一的元素,它不会削减它。

  2. 使用nth-child()或组合选择器字符串nth-of-type(),这会导致选择器很长。在大多数情况下,选择器越长,它的特异性就越高,而当 DOM 结构发生变化时,它的特异性越高,它就越有可能被破坏。

The solution below is an attempt at tackling both of these issues. It is a hybrid approach that outputs a unique CSS selector (i.e., document.querySelectorAll(getUniqueSelector(el))should always return a one-item array). While the returned selector string is not necessarily the shortest, it is derived with an eye towards CSS selector efficiency while balancing specificity by prioritizing nth-of-type()and nth-child()last.

下面的解决方案试图解决这两个问题。它是一种输出唯一 CSS 选择器的混合方法(即,document.querySelectorAll(getUniqueSelector(el))应始终返回一个单项数组)。虽然返回的选择器字符串不一定是最短的,但它是在着眼于 CSS 选择器效率的同时通过优先级nth-of-type()nth-child()最后来平衡特异性的。

You can specify what attributes to incorporate into the selector by updating the aAttrarray. The minimum browser requirement is IE 9.

您可以通过更新aAttr数组来指定要合并到选择器中的属性。最低浏览器要求是 IE 9。

function getUniqueSelector(elSrc) {
  if (!(elSrc instanceof Element)) return;
  var sSel,
    aAttr = ['name', 'value', 'title', 'placeholder', 'data-*'], // Common attributes
    aSel = [],
    // Derive selector from element
    getSelector = function(el) {
      // 1. Check ID first
      // NOTE: ID must be unique amongst all IDs in an HTML5 document.
      // https://www.w3.org/TR/html5/dom.html#the-id-attribute
      if (el.id) {
        aSel.unshift('#' + el.id);
        return true;
      }
      aSel.unshift(sSel = el.nodeName.toLowerCase());
      // 2. Try to select by classes
      if (el.className) {
        aSel[0] = sSel += '.' + el.className.trim().replace(/ +/g, '.');
        if (uniqueQuery()) return true;
      }
      // 3. Try to select by classes + attributes
      for (var i=0; i<aAttr.length; ++i) {
        if (aAttr[i]==='data-*') {
          // Build array of data attributes
          var aDataAttr = [].filter.call(el.attributes, function(attr) {
            return attr.name.indexOf('data-')===0;
          });
          for (var j=0; j<aDataAttr.length; ++j) {
            aSel[0] = sSel += '[' + aDataAttr[j].name + '="' + aDataAttr[j].value + '"]';
            if (uniqueQuery()) return true;
          }
        } else if (el[aAttr[i]]) {
          aSel[0] = sSel += '[' + aAttr[i] + '="' + el[aAttr[i]] + '"]';
          if (uniqueQuery()) return true;
        }
      }
      // 4. Try to select by nth-of-type() as a fallback for generic elements
      var elChild = el,
        sChild,
        n = 1;
      while (elChild = elChild.previousElementSibling) {
        if (elChild.nodeName===el.nodeName) ++n;
      }
      aSel[0] = sSel += ':nth-of-type(' + n + ')';
      if (uniqueQuery()) return true;
      // 5. Try to select by nth-child() as a last resort
      elChild = el;
      n = 1;
      while (elChild = elChild.previousElementSibling) ++n;
      aSel[0] = sSel = sSel.replace(/:nth-of-type\(\d+\)/, n>1 ? ':nth-child(' + n + ')' : ':first-child');
      if (uniqueQuery()) return true;
      return false;
    },
    // Test query to see if it returns one element
    uniqueQuery = function() {
      return document.querySelectorAll(aSel.join('>')||null).length===1;
    };
  // Walk up the DOM tree to compile a unique selector
  while (elSrc.parentNode) {
    if (getSelector(elSrc)) return aSel.join(' > ');
    elSrc = elSrc.parentNode;
  }
}

回答by skrat

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

由于不必要的突变,我以某种方式发现所有实现都不可读。在这里,我在 ClojureScript 和 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:

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

回答by user1593165

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