使用 jQuery 在 DOM 中查找最接近的下一个元素

Find closest next element in DOM with jQuery

在下面的例子中。我想为 #main#secondary.

这两个项目找到第一个 .invalid-feedback.valid-feedback

显然我对一般情况感兴趣,这就是我为 jQuery 编写原型扩展的原因。

$.fn.extend({
  closestNext: function (selector) {
    let found = null    
    let search = (el, selector) => {
      if (!el.length) return
      if (el.nextAll(selector).length) {
        found = el.nextAll(selector).first()
        return
      }
      search(el.parent(), selector)
    }

    search($(this), selector)
    return found
  }
})

// Proof
$('#main').closestNext('.invalid-feedback').text('main-invalid')
$('#secondary').closestNext('.invalid-feedback').text('secondary-invalid')
$('#main').closestNext('.valid-feedback').text('any-valid')
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div>
    <div>
        <input id="main"/>
    </div>
    <div class="dummy"></div>
    <div class="invalid-feedback"></div>
    <div>
        <input id="secondary"/>
    </div>
    <div class="invalid-feedback"></div>
</div>
<div class="valid-feedback"></div>

我写的东西看起来很复杂,我期待这种 DOM 遍历函数成为 jQuery 开箱即用的一部分。遗憾的是,我在说明书上没有找到相关的功能。

是否有更简单的方法来实现与 closestNext 相同的结果?

编辑

从更算法的角度来看,我正在寻找按以下顺序进行的树遍历函数,但其​​复杂性比我在示例中实现的要好。

.
├── A1
│   ├── B1
│   │   ├── C1
│   │   ├── C2
│   │   └── C3
│   ├── B2
│   │   ├── C4 <--- Entry point
│   │   ├── C5
│   │   └── C6
│   └── B3
│       ├── C7
│       ├── C8
│       └── C9
└── A2
    ├── B4 
    │   ├── C10
    │   └── C11
    └── B5
        ├── C12
        └── C13

C4入口点开始,探索顺序为:

>>> traverse(C4)
C5, C6, B3, C7, C8, C9, A2, B4, C10, C11, B5, C12, C13

我还没有机会 运行 这个(编辑:它已经 运行 并且已经进行了更正)但这会成功。我们有 2 个函数 - 第一个函数寻找与我们正在搜索的目标节点匹配的元素的兄弟姐妹。如果 getSiblings 没有 return 匹配目标元素,则第二个函数开始滚动并向上攀登节点树。 我希望这会有所帮助..享受



var getSiblings = function (elem,selector) {

    // Setup siblings array and start from the parent element of our input
    //this is under the assumption that you're only looking for an element that FOLLOWS #main/#secondary


    var sibling = elem,
    target = selector;

    // Loop through each sibling and return the target element if its found
    while (sibling) {

            if (sibling.hasClass(target) ) { //return the target element if its been located
                return sibling;
            }
        //since the element has not been located, move onto the next sibling
        if (sibling.next().length == 0) {
            return 0;
        } else {
            sibling = sibling.next();
        }

    }

    return 0; //failed to locate target
};

function firstOfClass(el,selector) {
   //our variables where startingPoint is #main/#secondary and target is what we're looking for
   var startingPoint = el,
   target = selector;
   while (startingPoint.parent().length) { 
//if it has a parent lets take a look using our getSiblings function
       var targetCheck = getSiblings(startingPoint, target);
       if ( targetCheck != 0 ) { //returns 0 if no siblings are found i.e we found a match
           return targetCheck;
       } else {
//lets keep ascending
           startingPoint = startingPoint.parent();
       }
   }
   return getSiblings(startingPoint, target);

}
//returns node if found, otherwise returns 0

firstOfClass( $('#main'),'valid-feedback' );

firstOfClass( $('#secondary'),'invalid-feedback' );

如您所见,解决方案不需要对您的 DOM 有任何深入了解,而是有条不紊地搜索目标(除非找到它停止 运行ning)并继续上升直到它已经有效地用尽了它的搜索。

不确定这是否完全有效,但我认为是这样,这在某种程度上取决于您的标记的复杂程度,但即便如此,我认为它会给您下一次有效或无效的反馈 类 相对于所讨论的元素,因为您的 ID 是唯一的,并且匹配集中的索引 + 1 应该是您要查找的内容。可能矫枉过正,但它应该在 DOM 树中获得下一个最接近的树,而不仅仅是兄弟姐妹,children 等。我放入 CSS 更改以验证它在做什么。

$("#main, #secondary").on("click", function(e) {
  var invalid = $(this).add('.invalid-feedback');
  var valid = $(this).add('.valid-feedback');
  $(invalid.get(invalid.index( $(this)) +1)).css("background", "black");
  $(valid.get(valid.index( $(this)) +1)).css("background", "blue");
});

所以,这可行,但绝对不干净。我必须确保不包括 "previous" 个节点,所以我将它们过滤掉...

我又添加了几个测试用例来证明它只作用于 "next" 项。

我会选择你现有的解决方案。

$.fn.extend({
  closestNext: function(selector, originalMe) {
    const me = $(this);
    // try next first
    let nextSibling = me.next(selector);
    if (nextSibling.length) return nextSibling;
    // try descendents
    let descendents = me.find(selector).not((i, e) => (originalMe || me).prevAll().is(e));
    if (descendents.length) return descendents.first();
    // try next parent
    const parent = me.parent().not((i, e) => (originalMe || me).parent().prevAll().is(e));
    return parent.closestNext(selector, me);
  }
})

// Proof
$('#main').closestNext('.invalid-feedback').text('main-invalid')
$('#secondary').closestNext('.invalid-feedback').text('secondary-invalid')
$('#main').closestNext('.valid-feedback').text('any-valid')
.dummy {
  color: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="valid-feedback dummy">should not change</div>
<div>
  <div class="invalid-feedback dummy">should not change</div>
  <div>
    <input id="main" />
  </div>
  <div class="dummy">should not change</div>
  <div class="invalid-feedback">should change</div>
  <div>
    <input id="secondary" />
  </div>
  <div class="invalid-feedback">should change</div>
</div>
<div class="valid-feedback">should change</div>

我看不出你的初始代码如何能更简单。毕竟,它需要迭代标记。

虽然我看到的是如何优化它,使用 return 代替 let found = null 并且只调用 el.nextAll(selector) 一次。

我还解决了找不到元素的情况,如果找不到,您最终会遇到异常。

堆栈片段

$.fn.extend({
  closestNext: function (selector) {
    let search = (el, selector) => {
      if (!el.length) return el
      let f = el.nextAll(selector)
      return f.length ? f.first() : search(el.parent(), selector)
    }
    return search($(this), selector)
  }
})

// Proof
$('#main').closestNext('.invalid-feedback').text('main-invalid')
$('#secondary').closestNext('.invalid-feedback').text('secondary-invalid')
$('#main').closestNext('.valid-feedback').text('any-valid')
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div>
    <div>
        <input id="main"/>
    </div>
    <div class="dummy"></div>
    <div class="invalid-feedback"></div>
    <div>
        <input id="secondary"/>
    </div>
    <div class="invalid-feedback"></div>
</div>
<div class="valid-feedback"></div>