从 Dom 元素获取 CSS 路径

IT技术 javascript css css-selectors
2021-03-12 14:30:12

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

但我得到了这样的东西:

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

但要完全正确,它应该是这样的:

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

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

6个回答

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

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

  • 当遇到第一个分配有 id 的祖先元素时停止
  • 用于nth-of-type()使选择器更具可读性
    var cssPath = 函数(el){
        if (!(el instanceof Element)) 
            返回;
        var 路径 = [];
        while (el.nodeType === Node.ELEMENT_NODE) {
            var 选择器 = el.nodeName.toLowerCase();
            如果(el.id){
                选择器 += '#' + el.id;
                path.unshift(选择器);
                休息;
            } 别的 {
                var sib = el, nth = 1;
                而 (sib = sib.previousElementSibling) {
                    if (sib.nodeName.toLowerCase() == 选择器)
                       第 ++;
                }
                如果(第n个!= 1)
                    选择器 += ":nth-of-type("+nth+")";
            }
            path.unshift(选择器);
            el = el.parentNode;
        }
        返回 path.join(" > ");
     }
@Sych,为什么?似乎工作正常,例如添加nth-of-type到“html”将不起作用。
2021-04-29 14:30:12
:nth-of-type()工作方式不同于:nth-child()- 有时用另一个替换一个并不是一件简单的事情。
2021-05-07 14:30:12
@jtblin,因为例如,.container span会捕获所有 span 的 inside .container,但.container span:nth-of-type(1) 只会捕获第一个,这可能是预期的行为。
2021-05-07 14:30:12
而不是:if (nth != 1)我们可以使用:if (el.previousElementSibling != null || el.nextElementSibling != null)nth-of-type(1)如果 element 是集合中的第一个元素,那么它将能够添加,但如果它是唯一的,则不会添加它。
2021-05-13 14:30:12
if (nth != 1) 不好,要拥有超特定的路径,即使它是 1,您也应该始终使用 child。
2021-05-15 14:30:12

要始终获得正确的元素,您将需要使用: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(" > ");
}

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

@jney:如果你的意思是:nth-child()选择器,那么不。
2021-04-26 14:30:12
是的,它看起来很棒。它也符合 IE 吗?
2021-05-09 14:30:12

另外两个提供的答案对我遇到的浏览器兼容性有几个假设。下面的代码不会使用 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(' > ');
}

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

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

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

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

您可以通过更新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;
  }
}
我要发表的一个评论是,虽然 id 属性应该是唯一的,但它不一定是静态的,因为某些站点使用在刷新之间更改的动态 ID。
2021-05-13 14:30:12

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

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