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

笑着哭i 提交于 2019-11-29 07:15:45

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/

Garet Claborn

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;
}

Maybe something like this would work:

$.fn.findExclude = function (Selector, Mask) {
    var result = new jQuery();
    $(this).each(function () {
        var $selected = $(this);
        $selected.find(Selector).filter(function (index) {
            var $closest = $(this).closest(Mask);
            return $closest.length == 0 || $closest[0] == $selected[0] || $.contains($closest, $selected);
        }).each(function () {
            result.push(this);
        });
    });
    return result;
}

http://jsfiddle.net/JCA23/

Chooses those elements that are either not in mask parent or their closest mask parent is same as root or their closest mask parent is a parent of root.

I think that this is the closest the findExclude can be optimized:

$.fn.findExclude = function (Selector, Mask) {
    var result = $([]);
    $(this).each(function (Idx, Elem) {
        $(Elem).find(Selector).each(function (Idx2, Elem2) {
            if ($(Elem2).closest(Mask)[0] == Elem) {
                result =  result.add(Elem2);
            }
        });
    });
    return result;
}

Also, see its fiddle with added logs with ellapsed time in milliseconds.

I see that you are worried with the performances. So, I've run some tests, and this implementation takes no longer than 2 milliseconds, while your implementation (as the answer you have posted) sometimes takes around 4~7 millisecods.

From my understanding, I would bind to the .controls elements and allow the event to bubble up to them. From that, you can get the closest .Interface to get the parent, if needed. This way you are added multiple handlers to the same elements as you go further down the rabbit hole.

While I saw you mention it, I never saw it implemented.

//Attach the event to the controls to minimize amount of binded events
$('.controls').on('click mouseenter mouseleave', function (event) { 
    var target = $(event.target),
        targetInterface = target.closest('.Interface'),
        role = target.data('role');

    if (event.type == 'click') {
        if (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;
            }
        }
    }
});

Here is a fiddle showing what I mean. I did remove your js in favor of a simplified display.

It does seem that my solution may be a over simplification though...


Update 2

So here is a fiddle that defines some common functions that will help achieve what you are looking for...I think. The getInterfaces provides a simplified function to find the interfaces and their controls, assuming all interfaces always have controls.

There are probably fringe cases that will creep up though. I also feel I need to apologize if you have already ventured down this path and I'm just not seeing/understanding!


Update 3

Ok, ok. I think I understand what you want. You want to get the unique interfaces and have a collection of controls that belong to it, that make sense now.

Using this fiddle as the example, we select both the .Interface and the .Interface .controls.

var interfacesAndControls = $('.Interface, .Interface .controls');

This way we have a neat collection of the interfaces and the controls that belong to them in order they appear in the DOM. With this we can loop through the collection and check to see if the current element has the .Interface associated with it. We can also keep a reference to the current interface object we create for it so we can add the controls later.

if (el.hasClass('Interface')){
    currentInterface = new app.Interface(el, [], eventCallback);

    interfaces.push(currentInterface);

    //We don't need to do anything further with the interface
    return;
};

Now when we don't have the .Interface class associate with the element, we got controls. So let's first modify our Interface object to support adding controls and binding events to the controls as they are being added to the collection.

//The init function was removed and the call to it
self.addControls = function(el){
    //Use the mouseover and mouseout events so event bubbling occurs
    el.on('click mouseover mouseout', self.eventCallback)
    self.controls.push(el);
}

Now all we have to do is add the control to the current interfaces controls.

currentInterface.addControls(el);

After all that, you should get an array of 3 objects (interfaces), that have an array of 2 controls each.

Hopefully, THAT has everything you are looking for!

If I understand you:

understanding your needs better and applying the specific classes you need, I think this is the syntax will work:

var targetsOfTopGroups  = $('.InterfaceGroup .Interface:not(.Interface .Interface):not(.Interface .InterfaceGroup)')

This Fiddle is an attempt to reproduce your scenario. Feel free to play around with it.


I think I found the problem. You were not including the buttons in your not selector

I changed the binding to be

        var Controls = $('.InterfaceGroup .Interface :button:not(.Interface .Interface :button):not(.Interface .InterfaceGroup :button)');

Fiddle

Why not taking the problem upside down?

Select all $(.target) elements and then discard them from further treatment if their .$parents(.group) is empty, that would give sonething like:

$('.target').each(function(){
    if (! $(this).parents('.group').length){
        //the jqueryElem is empy, do or do not
    } else {
        //not empty do what you wanted to do
    }
});

Note that don't answer the title but literally gives you "Selector B, inside of a result from Selector A"

If your .interface classes had some kind of identifier this would seem to be rather easy. Perhabs you already have such an identifier for other reasons or choose to include one.

http://jsfiddle.net/Dc4dz/

<div class="interface" name="a">
    <div class="control">control</div>
    <div class="branch">
        <div class="control">control</div>
        <div class="interface">
            <div class="branch">
                <div class="control">control</div>
            </div>
        </div>
    </div>
    <div class="interface" name="c">
        <div class="branch">
            <div class="control">control</div>
        </div>
    </div> </div>

$( ".interface[name=c] .control:not(.interface[name=c] .interface .control)" ).css( "background-color", "red" );
$( ".interface[name=a] .control:not(.interface[name=a] .interface .control)" ).css( "background-color", "green" );

Edit: And now Im wondering if you're tackling this problem from the wrong angle.

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

http://jsfiddle.net/Dc4dz/1/

$(".interface").on("click", ".control", function (event) {
    alert($(this).text());
    event.stopPropagation();
});

The event would be triggered on a .control; it would then bubble up to its .closest( ".interface" ) where it would be processed and further propagation be stopped. Isn't that what you described?

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!