Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Skip recursion in jQuery.find() for a selector?

TL;DR: How do I get an action like find(), but block traversal (not full stop, just skip) for a certain selector?

ANSWERS: $(Any).find(Selector).not( $(Any).find(Mask).find(Selector) )

There were many truly great answers, I wish I could some how distribute the bounty points more, maybe I should make some 50 pt bounties in response to some of these ;p I choose Karl-André Gagnon's because this answer managed to make findExclude unrequired in one, slightly long, line. While this uses three find calls and a heavy not filter, in most situations jQuery can use very fast implementation that skips traversal for most find()s.

Especially good answers are listed below:

falsarella: Good improvement on my solution, findExclude(), best in many situatoins

Zbyszek: A filter-based solution similar to falsarella's, also good on efficiency

Justin: A completely different, but manageable and functional solution to the underlaying issues

Each of these have their own unique merits and and are deserving of some mention.

I need to descend into an element fully and compare selectors, returning all matched selectors as an array, but skip descending into the tree when another selector is encountered.

Selection Path IllustrationEdit: replacing original code sample with some from my site

This is for a message forum which may have reply message-groups nested inside any message.
Notice, however, we cannot use the message or content classes because the script is also used for other components outside of the forum. Only InterfaceGroup, Interface and controls classes are potentially useful - and preferably just Interface and controls.

Interact with the code and see it in JS Fiddle, thanks Dave A, here Click on the buttons while viewing a JavaScript console to see that the controls class is being bound to one extra time per level of .Interface nesting.

Visual A, Forum Layout Struture:

    <li class="InterfaceGroup">
        <ul class="Interface Message" data-role="MessagePost" >
            <li class="instance"> ... condensed ... </li>
            <li class="InterfaceGroup"> ... condensed ...</li>
        </ul>
        <ul class="Interface Message" data-role="MessagePost" >
            <li class="instance"> ... condensed ... </li>
        </ul>
        <ul class="Interface Message" data-role="MessagePost" >
            <li class="instance"> ... condensed ... </li>
            <li class="InterfaceGroup"> ... condensed ...</li>
        </ul>

    </li>

Inside of each <li class="InterfaceGroup"> there could be any number of repetitions of the same structure (each group is a thread of messages) and/or deeper nesting such as..

    <li class="InterfaceGroup">

        <ul class="Interface Message" data-role="MessagePost" >
            <li class="instance"> ... condensed ... </li>
            <li class="InterfaceGroup">

                <ul class="Interface Message" data-role="MessagePost" >
                    <li class="instance"> ... condensed ... </li>
                    <li class="InterfaceGroup"> ... condensed ...</li>
                </ul>
            </li>
        </ul>
    </li>

Inside of each <li class="instance"> ... </li> there are arbitrary places decided by another team where class="controls" may appear and an event listener should be bound. Though these contain messages, other components structure their markup arbitrarily but will always have .controls inside of .Interface, which are collected into an .InterfaceGroup.A reduced-complexity version of the inner-content (for forum posts) is below for reference.

Visual B, Message Contents with controls class:

<ul class="Interface Message" data-role="MessagePost" >
    <li class="instance">
      <ul class="profile"> ...condensed, nothing clickable...</ul>
      <ul class="contents">
        <li class="heading"><h3>Hi there!</h3></li>
        <li class="body"><article>TEST Message here</article></li>
        <li class="vote controls">
          <button class="up" data-role="VoteUp" ><i class="fa fa-caret-up"> </i><br/>1</button>
          <button class="down" data-role="VoteDown" >0<br/><i class="fa fa-caret-down"> </i></button>
        </li>
        <li class="social controls">
          <button class="reply-btn" data-role="ReplyButton" >Reply</button>
        </li>
      </ul>
    </li>
    <li class="InterfaceGroup" >    <!-- NESTING OCCURRED -->
      <ul class="Interface Message" data-role="MessagePost" >
          <li class="instance">... condensed ... </li>
          <li class="InterfaceGroup" >... condensed ... </li>
      </ul>
   </li>
</ul>

We can only bind to controls that are within an Interface class, instance may or may not exist but Interface will. Events bubble to .controls elements and have a reference to the .Interface which holds them..

So I am trying to $('.Interface').each( bind to any .controls not inside a deeper .Interface )

That's the tricky part, because

  • .Interface .controls will select the same .control multiple times in the .each()
  • .not('.Interface .Interface .controls') cancels out controls in any deeper nesting

How can I do this using jQuery.find() or a similar jQuery method for this?

I have been considering that, perhaps, using children with a not selector could work and could be doing the same thing as find under the hood, but I'm not so sure that it actually is or wont cause horrible performance. Still, an answer recursing .children effectively is acceptable.

UPDATE: Originally I tried to use a psuedo-example for brevity, but hopefully seeing a forum structure will help clarify the issue since they're naturally nested structures. Below I'm also posting partial javascript for reference, line two of the init function is most important.

Reduced JavaScript partial:

var Interface=function()
{
    $elf=this;

    $elf.call=
    {
        init:function(Markup)
        {
            $elf.Interface = Markup;
            $elf.Controls = $(Markup).find('.controls').not('.Interface .controls');
            $elf.Controls.on('click mouseenter mouseleave', function(event){ $elf.call.events(event); });
            return $elf;
        },
        events:function(e)
        {
            var classlist = e.target.className.split(/\s+/), c=0, L=0;
            var role = $(e.target).data('role');

            if(e.type == 'click')
            {
                CurrentControl=$(e.target).closest('[data-role]')[0];
                role = $(CurrentControl).data('role');

                switch(role)
                {
                    case 'ReplyButton':console.log('Reply clicked'); break;
                    case 'VoteUp':console.log('Up vote clicked'); break;
                    case 'VoteDown':console.log('Down vote clicked'); break;
                    default: break;
                }
            }
        }
    }
};

$(document).ready( function()
{
    $('.Interface').each(function(instance, Markup)
    {
        Markup.Interface=new Interface().call.init(Markup);
    });
} );
like image 558
Garet Claborn Avatar asked Jun 11 '14 09:06

Garet Claborn


2 Answers

If you want to exclude element in you find, you can use a not filter. As for example, I've taken you function that exclude element and made it way shorter :

$.fn.findExclude = function( Selector, Mask,){
    return this.find(Selector).not(this.find(Mask).find(Selector))
}

Now, ill be honest with you, I did not fully understand what you want. But, when i took a look at your function, I saw what you were trying to do.

Anyway, take a look at this fiddle, the result is the same as your : http://jsfiddle.net/KX65p/8/

like image 51
Karl-André Gagnon Avatar answered Sep 27 '22 18:09

Karl-André Gagnon


Well, I really don't want to be answering my own question on a bounty, so if anyone can provide a better or alternative implementation please do..

However, being pressed to complete the project, I ended up working on this quite a bit and came up with a fairly clean jQuery plugin for doing a jQuery.find() style search while excluding child branches from the results as you go.

Usage to work with sets of elements inside nested views:

// Will not look in nested ul's for inputs
$('ul').findExclude('input','ul');

// Will look in nested ul's for inputs unless it runs into class="potato"
$('ul').findExclude('input','.potato');

More complex example found at http://jsfiddle.net/KX65p/3/ where I use this to .each() a nested class and bind elements which occur in each nested view to a class. This let me make components server-side and client-side reflect each other's properties and have cheaper nested event handling.

Implementation:

// Find-like method which masks any descendant
// branches matching the Mask argument.
$.fn.findExclude = function( Selector, Mask, result){

    // Default result to an empty jQuery object if not provided
    result = typeof result !== 'undefined' ?
                result :
                new jQuery();

    // Iterate through all children, except those match Mask
    this.children().each(function(){

        thisObject = jQuery( this );
        if( thisObject.is( Selector ) ) 
            result.push( this );

        // Recursively seek children without Mask
        if( !thisObject.is( Mask ) )
            thisObject.findExclude( Selector, Mask, result );
    });

    return result;
}

(Condensed Version):

$.fn.findExclude = function( selector, mask, result )
{
    result = typeof result !== 'undefined' ? result : new jQuery();
    this.children().each( function(){
        thisObject = jQuery( this );
        if( thisObject.is( selector ) ) 
            result.push( this );
        if( !thisObject.is( mask ) )
            thisObject.findExclude( selector, mask, result );
    });
    return result;
}
like image 24
Garet Claborn Avatar answered Sep 27 '22 18:09

Garet Claborn