Search code examples
javascriptdomcss-selectors

DOM: Iterate over elements excluding nested elements that match the same selector


(PLEASE NO JQUERY) Let's say I have the following tree. I want to iterate over elements that match querySelectorAll('div.bar') starting from div.foo, but not over children that match the same selector. How do I do that?

div.foo
  div.bar -- select
  div.bar -- select
    div.bar -- skip
    div.bar -- skip
  div.damnSon
    div.bar -- select
    div.bar -- select
       div.bar --skip
  div.bar -- select
    div.bar -- skip

TreeWalker is a no-go because

  • it rejects the element entirely, while I still want to process it, just not it's children
  • it only works on visible elements, while I may be iterating over hidden ones too NodeIterator don't work either because of the same issues and it iterates over children all the same, even when you reject the parent.

Most ideal case would be to somehow get the result of querySelector that isn't flat. Then I'd just loop over them without having to concern myself with nested properties.


Solution

  • So far I have come up with the following alcoholic method:

    let nodeIterator = document.createNodeIterator(root, NodeFilter.SHOW_ELEMENT);
    let node;
    while(node = nodeIterator.nextNode()){
      if (node.matches('.bar .bar')) {
        //skip
      } else if (node.matches('.bar')) {
        //do things
      }
    }
    

    The problems with this are:

    • it's slow - especially in SVGs or anything that has many elements
    • it's still flat.
    • it will break if div.foo (i.e. my root element) is itself nested inside (div.bar)

    If anyone can advise a better method, that'd be much appreciated.


    UPDATE: Ok so I came up with the following method, that sort of solves the last point without being that much more inefficient:

    let bars = root.getElementsByClassName('bar');
    let excludedChildren = [];
    for(let i = 0; i < bars.length; i++){
      let children = bars[i].getElementsByClassName('bar');
      if(children.length>0) Array.prototype.push.apply(excludedChildren, children);
    }
    bars = bars.filter(e=>!excludedChildren.includes(e));
    bars.forEach(e=>{
      //do stuff
    });
    

    UPDATE: Time to close this old sh*t up, here is my final solution. Has worked for a month now in production without any problems. Of course this will not be able to handle some very complex cases, but it's extensible.