【问题标题】:Get CSS path from Dom element从 Dom 元素获取 CSS 路径
【发布时间】:2011-04-06 21:56:39
【问题描述】:

我得到了这个函数来获取一个 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 中实现它吗?

【问题讨论】:

  • 如果你想要一个 CSS 选择器,它可能应该是 :eq(1):nth-child(2) 而不是 [1]
  • 或者只是用 JavaScript 给元素一个唯一的 ID?我可以理解为什么 cssPath 作为 FireBug 插件或其他东西可能很有用,但对于常规代码,引入 ID 是最有效的。
  • 事实上,我相信有一个 FireBug 插件可以从一个名为 FireFinder 的元素中获取 cssPath ;oP
  • simmer.js 看起来是一个很好的库。

标签: javascript css css-selectors


【解决方案1】:

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

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

  • 在遇到第一个分配了 id 的祖先元素时停止
  • 使用nth-of-type() 使选择器更具可读性
var cssPath = 函数(el){ if (!(el instanceof Element)) 返回; 变量路径 = []; 而(el.nodeType === Node.ELEMENT_NODE){ var 选择器 = el.nodeName.toLowerCase(); 如果(el.id){ 选择器 += '#' + el.id; path.unshift(选择器); 休息; } 别的 { var sib = el, nth = 1; 而(同胞=同胞.previousElementSibling){ if (sib.nodeName.toLowerCase() == 选择器) 第n个++; } 如果(第n个!= 1) 选择器 += ":nth-of-type("+nth+")"; } path.unshift(选择器); el = el.parentNode; } 返回路径.join(" > "); }

【讨论】:

  • :nth-of-type():nth-child() 的工作方式不同 - 有时这不是用另一个替换一个简单的问题。
  • if (nth != 1) 不好,要拥有超特定路径,即使它是 1,您也应该始终使用 child。
  • @Sych,为什么?似乎工作正常,例如将nth-of-type 添加到“html”将不起作用。
  • @jtblin,因为,例如,.container span 会捕获 .container 内的所有 span,但 .container span:nth-of-type(1) 只会捕获第一个,这可能是预期的行为。
  • 代替:if (nth != 1) 我们可以使用:if (el.previousElementSibling != null || el.nextElementSibling != null)。如果元素是集合中的第一个元素,那么它将能够添加nth-of-type(1),但如果它是唯一的元素,则不会添加它。
【解决方案2】:

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

【讨论】:

  • 是的,它看起来很棒。它也兼容 IE 吗?
  • @jney:如果你的意思是:nth-child() 选择器,那么没有。
【解决方案3】:

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

【讨论】:

    【解决方案4】:

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

    1. 从元素名称、类和idname 属性的组合中向上爬上DOM 树组合选择器字符串。这种方法的问题在于,它可能导致选择器返回多个元素,如果我们要求它们只选择一个唯一元素,这将无法解决。

    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。
    【解决方案5】:

    由于不必要的突变,我不知何故发现所有实现都无法阅读。这里我用 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(" > ");
      }
    };
    

    【讨论】:

      【解决方案6】:

      有一些 js 库正是这样做的:

      我正在使用第一个并且到目前为止成功

      【讨论】:

        【解决方案7】:
        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;
        };      
        

        【讨论】:

          猜你喜欢
          • 2021-05-29
          • 2014-08-03
          • 2020-12-18
          • 2022-12-05
          • 1970-01-01
          • 2021-03-20
          • 2014-11-24
          • 2022-01-17
          相关资源
          最近更新 更多