点击 <a> 元素后获取其 DOM 路径

29

HTML

<body>
<div class="lol">
<a class="rightArrow" href="javascriptVoid:(0);" title"Next image">
</div>
</body>

伪代码

$(".rightArrow").click(function() {
rightArrowParents = this.dom(); //.dom(); is the pseudo function ... it should show the whole
alert(rightArrowParents);
});

警告信息将是:

body div.lol a.rightArrow

我如何使用javascript/jquery获取它?


几乎重复:https://dev59.com/Y2855IYBdhLWcg3wPBtx - Lightness Races in Orbit
1
请查看此处:https://dev59.com/vHA75IYBdhLWcg3wJFYL#32623171 - iimos
12个回答

52

以下是一个返回jQuery路径的本地JS版本。如果元素具有ID,我还会为其添加ID。如果您在数组中看到ID,这将为您提供执行最短路径的机会。

var path = getDomPath(element);
console.log(path.join(' > '));

输出

body > section:eq(0) > div:eq(3) > section#content > section#firehose > div#firehoselist > article#firehose-46813651 > header > h2 > span#title-46813651

这里是函数。

function getDomPath(el) {
  var stack = [];
  while ( el.parentNode != null ) {
    console.log(el.nodeName);
    var sibCount = 0;
    var sibIndex = 0;
    for ( var i = 0; i < el.parentNode.childNodes.length; i++ ) {
      var sib = el.parentNode.childNodes[i];
      if ( sib.nodeName == el.nodeName ) {
        if ( sib === el ) {
          sibIndex = sibCount;
        }
        sibCount++;
      }
    }
    if ( el.hasAttribute('id') && el.id != '' ) {
      stack.unshift(el.nodeName.toLowerCase() + '#' + el.id);
    } else if ( sibCount > 1 ) {
      stack.unshift(el.nodeName.toLowerCase() + ':eq(' + sibIndex + ')');
    } else {
      stack.unshift(el.nodeName.toLowerCase());
    }
    el = el.parentNode;
  }

  return stack.slice(1); // removes the html element
}

1
此函数表现良好,我认为比接受的答案更好,因为它允许您获取模棱两可元素的唯一路径。例如,一个无序列表上没有唯一标识符的列表项。 - Adam
我同意@Adam的观点,这个函数可以正确地定位到相同元素中的目标元素(ul>li:eq(1)>a)。太棒了! - Cyril N.
对于我的情况,我必须检查元素是否具有属性函数:typeof el.hasAttribute === 'function',然后才能检查 el.hasAttribute('id'). - The gates of Zion

37

使用jQuery,像这样(以下是一种不使用jQuery除事件外的解决方案;如果重复函数调用很重要,则调用更少的函数):

使用jQuery,像这样(下面提供了一种不使用jQuery除事件外的解决方案;如果函数调用次数很重要,则可以调用较少的函数):

$(".rightArrow").click(function () {
    const rightArrowParents = [];
    $(this)
        .parents()
        .addBack()
        .not("html")
        .each(function () {
            let entry = this.tagName.toLowerCase();
            const className = this.className.trim();
            if (className) {
                entry += "." + className.replace(/ +/g, ".");
            }
            rightArrowParents.push(entry);
        });
    console.log(rightArrowParents.join(" "));
    return false;
});

实时示例:

$(".rightArrow").click(function () {
    const rightArrowParents = [];
    $(this)
        .parents()
        .addBack()
        .not("html")
        .each(function () {
            let entry = this.tagName.toLowerCase();
            const className = this.className.trim();
            if (className) {
                entry += "." + className.replace(/ +/g, ".");
            }
            rightArrowParents.push(entry);
        });
    console.log(rightArrowParents.join(" "));
    return false;
});
<div class="   lol   multi   ">
    <a href="#" class="rightArrow" title="Next image">Click here</a>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>

(在实时例子中,我已经更新了 div 上的 class 属性为 lol multi, 以演示处理多个类的方法。)

代码使用 parents 获取被点击元素的祖先元素,通过 not 移除其中的 html 元素(因为你从 body 开始),然后循环创建每个父级元素的条目,并将其推入数组。接着使用 addBacka 添加回集合中,这也改变了集合的顺序到你所需的顺序(parents 得到的是你想要的父级元素的反向顺序,但是 addBack 可以让它按 DOM 的正常顺序排列)。最后使用 Array#join 创建以空格分隔的字符串。

在创建条目时,我们会修剪 className(因为在 class 属性中前导和尾随空格保留,但没有意义),然后如果还有剩余内容,就使用点号(.)将一个或多个空格的系列替换为点号,以支持具有多个类的元素(例如 <p class='foo bar'>className"foo bar",所以该条目最终是 p.foo.bar)。

只是为了完整性起见,这是 jQuery 可能过于繁琐的情况之一,你可以直接遍历 DOM 树来实现这个效果:

$(".rightArrow").click(function () {
    const rightArrowParents = [];

    for (let elm = this; elm; elm = elm.parentNode) {
        let entry = elm.tagName.toLowerCase();
        if (entry === "html") {
            break;
        }
        const className = elm.className.trim();
        if (className) {
            entry += "." + className.replace(/ +/g, ".");
        }
        rightArrowParents.push(entry);
    }
    rightArrowParents.reverse();
    console.log(rightArrowParents.join(" "));
    return false;
});

实时示例:

$(".rightArrow").click(function () {
    const rightArrowParents = [];

    for (let elm = this; elm; elm = elm.parentNode) {
        let entry = elm.tagName.toLowerCase();
        if (entry === "html") {
            break;
        }
        const className = elm.className.trim();
        if (className) {
            entry += "." + className.replace(/ +/g, ".");
        }
        rightArrowParents.push(entry);
    }
    rightArrowParents.reverse();
    console.log(rightArrowParents.join(" "));
    return false;
});
<div class="   lol   multi   ">
    <a href="#" class="rightArrow" title="Next image">Click here</a>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>

在那里,我们只需使用标准的parentNode属性(或者我们可以使用parentElement)来反复向上遍历树,直到我们没有父级或者看到html元素。然后,我们将数组反转(因为它与您想要的输出是相反的),并加入它,就可以了。


5
我认为你已经知道你是一个牛逼的人。谢谢啊! :) - Tomkay
对于你的纯JS示例,使用rightArrowParents.unshift()而不是push()reverse()可能会更高效? - p0lar_bear
@p0lar_bear - 我怀疑不会,因为这需要在循环中每次重新洗牌所有属性,而不是在最后一次进行。但是考虑到数量很少,我也怀疑这是否重要。 :-) - T.J. Crowder
1
实际上,甚至更好的方法是this.className.trim().replace(/ {2,}/g, ' '),可以替换类名之间多余的空格。 - QuantumBlack
1
@QuantumBlack - 又一个好观点。我选择在已经被修剪过的className上使用 entry += className.replace(/ +/g, "."); (因为我在测试它是否为空之前修剪过它)。 - T.J. Crowder

14

我需要一个返回CSS标准路径(非jQuery)并处理ShadowDOM的原生JS版本。这段代码是对Michael Connor答案的小更新,以防有人需要:

function getDomPath(el) {
  if (!el) {
    return;
  }
  var stack = [];
  var isShadow = false;
  while (el.parentNode != null) {
    // console.log(el.nodeName);
    var sibCount = 0;
    var sibIndex = 0;
    // get sibling indexes
    for ( var i = 0; i < el.parentNode.childNodes.length; i++ ) {
      var sib = el.parentNode.childNodes[i];
      if ( sib.nodeName == el.nodeName ) {
        if ( sib === el ) {
          sibIndex = sibCount;
        }
        sibCount++;
      }
    }
    // if ( el.hasAttribute('id') && el.id != '' ) { no id shortcuts, ids are not unique in shadowDom
    //   stack.unshift(el.nodeName.toLowerCase() + '#' + el.id);
    // } else
    var nodeName = el.nodeName.toLowerCase();
    if (isShadow) {
      nodeName += "::shadow";
      isShadow = false;
    }
    if ( sibCount > 1 ) {
      stack.unshift(nodeName + ':nth-of-type(' + (sibIndex + 1) + ')');
    } else {
      stack.unshift(nodeName);
    }
    el = el.parentNode;
    if (el.nodeType === 11) { // for shadow dom, we
      isShadow = true;
      el = el.host;
    }
  }
  stack.splice(0,1); // removes the html element
  return stack.join(' > ');
}

1
警告:shadow DOM v1不再支持::shadow选择器。没有办法为shadow DOM元素生成完整的CSS选择器。我建议将上面的“::shadow”行替换为错误信息。 - Aleksandar Totic

3

下面是一个精确匹配元素的解决方案。

重要的是要理解,在DOM中,浏览器工具显示的选择器它不是一个真实的选择器)不能唯一地标识一个元素。(例如,它不能区分连续的span元素列表。没有位置/索引信息

这是一个类似(关于xpath)回答的改编。

$.fn.fullSelector = function () {
    var path = this.parents().addBack();
    var quickCss = path.get().map(function (item) {
        var self = $(item),
            id = item.id ? '#' + item.id : '',
            clss = item.classList.length ? item.classList.toString().split(' ').map(function (c) {
                return '.' + c;
            }).join('') : '',
            name = item.nodeName.toLowerCase(),
            index = self.siblings(name).length ? ':nth-child(' + (self.index() + 1) + ')' : '';

        if (name === 'html' || name === 'body') {
            return name;
        }
        return name + index + id + clss;

    }).join(' > ');

    return quickCss;
};

您可以像这样使用它

console.log( $('some-selector').fullSelector() );

请看此链接获取示例。


3

我最终使用的是简短的原生ES6版本:

返回与我在Chrome检查器中所看到的输出相同的结果,例如body div.container input#name

function getDomPath(el) {
  let nodeName = el.nodeName.toLowerCase();
  if (el === document.body) return 'body';
  if (el.id) nodeName += '#' + el.id;
  else if (el.classList.length) 
    nodeName += '.' + [...el.classList].join('.');
  return getDomPath(el.parentNode) + ' ' + nodeName;
};

我喜欢这个解决方案。 - Nikster

2

我一直在使用Michael Connor的答案,并对其进行了一些改进。

  • 使用ES6语法
  • 使用nth-of-type代替nth-child,因为nth-of-type只查找相同类型的子元素,而不是任何子元素
  • 以更清晰的方式删除html节点
  • 忽略具有id的元素的nodeName
  • 仅显示到最近的id,如果有的话。这应该使代码更加健壮,但我在哪一行留下了注释,如果您不想要这种行为,请删除该行
  • 使用CSS.escape处理ID和节点名称中的特殊字符

~

export default function getDomPath(el) {
  const stack = []

  while (el.parentNode !== null) {
    let sibCount = 0
    let sibIndex = 0
    for (let i = 0; i < el.parentNode.childNodes.length; i += 1) {
      const sib = el.parentNode.childNodes[i]
      if (sib.nodeName === el.nodeName) {
        if (sib === el) {
          sibIndex = sibCount
          break
        }
        sibCount += 1
      }
    }

    const nodeName = CSS.escape(el.nodeName.toLowerCase())

    // Ignore `html` as a parent node
    if (nodeName === 'html') break

    if (el.hasAttribute('id') && el.id !== '') {
      stack.unshift(`#${CSS.escape(el.id)}`)
      // Remove this `break` if you want the entire path
      break
    } else if (sibIndex > 0) {
      // :nth-of-type is 1-indexed
      stack.unshift(`${nodeName}:nth-of-type(${sibIndex + 1})`)
    } else {
      stack.unshift(nodeName)
    }

    el = el.parentNode
  }

  return stack
}

这个实际上比被接受的答案要好得多,说实话... - Nikster
@whichdan 如果你能把它打包成NPM就太好了。如果你没有时间,我可以帮你做,但我更愿意让你得到荣誉。 - Yanis-git

2

其他答案中的所有示例对我来说都不太正确,所以我自己制作了一个版本,也许我的版本会更适合其他人。

const getDomPath = element => {
  let templateElement = element
    , stack = []

  for (;;) {
    if (!!templateElement) {
      let attrs = ''
      for (let i = 0; i < templateElement.attributes.length; i++) {
        const name = templateElement.attributes[i].name  
        if (name === 'class' || name === 'id') {
            attrs += `[${name}="${templateElement.getAttribute(name)}"]`   
        }
      }

      stack.push(templateElement.tagName.toLowerCase() + attrs)
      templateElement = templateElement.parentElement
    } else {
      break
    }
  }

  return stack.reverse().slice(1).join(' > ')
}

const currentElement = document.querySelectorAll('[class="serp-item__thumb justifier__thumb"]')[7]

const path = getDomPath(currentElement)

console.log(path)

console.log(document.querySelector(path))
console.log(currentElement)

2

我将T.J. Crowder的代码片段转移到了一个小型的jQuery插件中。虽然他说这完全是不必要的开销,但我还是使用了他的jQuery版本,因为我只是用它来进行调试,所以我不在乎。

用法:

Html

<html>
<body>
    <!-- Two spans, the first will be chosen -->
    <div>
        <span>Nested span</span>
    </div>
    <span>Simple span</span>

    <!-- Pre element -->
    <pre>Pre</pre>
</body>
</html>

Javascript

// result (array): ["body", "div.sampleClass"]
$('span').getDomPath(false)

// result (string): body > div.sampleClass
$('span').getDomPath()

// result (array): ["body", "div#test"]
$('pre').getDomPath(false)

// result (string): body > div#test
$('pre').getDomPath()

Repository

https://bitbucket.org/tehrengruber/jquery.dom.path


0
    var obj = $('#show-editor-button'),
       path = '';
    while (typeof obj.prop('tagName') != "undefined"){
        if (obj.attr('class')){
            path = '.'+obj.attr('class').replace(/\s/g , ".") + path;
        }
        if (obj.attr('id')){
            path = '#'+obj.attr('id') + path;
        }
        path = ' ' +obj.prop('tagName').toLowerCase() + path;
        obj = obj.parent();
    }
    console.log(path);

0

你好,这个函数解决了当前元素未显示在路径中的错误。

现在请检查一下。

$j(".wrapper").click(function(event) {
      selectedElement=$j(event.target);

      var rightArrowParents = [];
      $j(event.target).parents().not('html,body').each(function() {
          var entry = this.tagName.toLowerCase();
          if (this.className) {
              entry += "." + this.className.replace(/ /g, '.');
          }else if(this.id){
              entry += "#" + this.id;
          }
          entry=replaceAll(entry,'..','.');
          rightArrowParents.push(entry);
      });
      rightArrowParents.reverse();
      //if(event.target.nodeName.toLowerCase()=="a" || event.target.nodeName.toLowerCase()=="h1"){
        var entry = event.target.nodeName.toLowerCase();
        if (event.target.className) {
              entry += "." + event.target.className.replace(/ /g, '.');
        }else if(event.target.id){
              entry += "#" + event.target.id;
        }
        rightArrowParents.push(entry);
     // }

其中$j = jQuery变量

同时解决类名中的..问题

以下是替换函数:

function escapeRegExp(str) {
    return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
}
  function replaceAll(str, find, replace) {
  return str.replace(new RegExp(escapeRegExp(find), 'g'), replace);
}

谢谢


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接