Пропустить рекурсию в jQuery.find() для селектора?

TL;DR: Как получить действие типа find(), но заблокировать обход (не полный останов, просто пропустить) для определенного селектора?

ОТВЕТЫ: $(Any).find(Selector).not( $(Any).find(Mask).find(Selector) )

Было много по-настоящему хороших ответов, я хотел бы кое-что как распределить баллы щедрости, может быть, я должен был получить вознаграждение в размере 50 пунктов в ответ на некоторые из них;p Я выбрал Карла-Андре Ганьона, потому что из-за этого ответа в findExclude не было необходимости одна, слегка длинная линия. Хотя для этого используются три вызова find и тяжелый фильтр not, в большинстве случаев jQuery может использовать очень быструю реализацию, которая пропускает обход большинства find ().

Особенно хорошие ответы перечислены ниже:

falsarella: хорошее улучшение моего решения, findExclude(), лучший во многих ситуациях

Zbyszek: решение на основе фильтров, похожее на фальзареллы, также хорошее по эффективности

Джастин: совершенно другое, но управляемое и функциональное решение основных проблем

Каждый из них имеет свои уникальные достоинства и заслуживает отдельного упоминания.

Мне нужно полностью погрузиться в элемент и сравнить селекторы, возвращая все совпадающие селекторы в виде массива, но пропустить спуск в дерево при обнаружении другого селектора.

Иллюстрация выбора пути Редактировать: заменить оригинальный пример кода с некоторых с моего сайта

Это для форума сообщений, который может иметь группы ответных сообщений, вложенные в любое сообщение.
Заметьте, однако, что мы не можем использовать сообщения или классы контента, потому что скрипт также используется для других компонентов вне форума. Только InterfaceGroup, Interface а также controls классы потенциально полезны - и желательно только интерфейс и элементы управления.

Взаимодействуйте с кодом и посмотрите его в JS Fiddle, спасибо Дейву А, здесь. Нажмите на кнопки во время просмотра консоли JavaScript, чтобы увидеть, что класс элементов управления привязан к одному дополнительному времени на уровень вложенности.Interface.

Visual A, Форум Макет 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>

Внутри каждого <li class="InterfaceGroup"> может быть любое количество повторений одной и той же структуры (каждая группа представляет собой поток сообщений) и / или более глубокое вложение, например..

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

Внутри каждого <li class="instance"> ... </li> Есть произвольные места, выбранные другой командой, где class="controls" может появиться, и слушатель события должен быть привязан. Хотя они содержат сообщения, другие компоненты структурируют свою разметку произвольно, но всегда будут иметь .controls Внутри .Interface, которые собраны в .InterfaceGroup Ниже приведена версия внутреннего контента с уменьшенной сложностью (для сообщений на форуме).

Visual B, Содержимое сообщения с классом управления:

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

Мы можем связать только элементы управления, которые находятся в классе интерфейса, instance может или не может существовать, но интерфейс будет. События пузыря .controls элементы и имеют ссылку на .Interface который держит их.,

Поэтому я пытаюсь $('.Interface').each( bind to any .controls not inside a deeper .Interface )

Это сложная часть, потому что

  • .Interface .controls выберу то же самое .control несколько раз в.each ()
  • .not ('. Interface.Interface.controls') отменяет элементы управления в любой более глубокой вложенности

Как я могу сделать это, используя jQuery.find() или аналогичный метод jQuery для этого?

Я думал о том, что, возможно, использование детей с неселектором может сработать и может сделать то же самое, что найти под капотом, но я не уверен, что это на самом деле или не приведет к ужасной производительности. Тем не менее, ответный ответ.children эффективно является приемлемым.

ОБНОВЛЕНИЕ: Первоначально я попытался использовать псевдо-пример для краткости, но, надеюсь, просмотр структуры форума поможет прояснить проблему, поскольку они являются естественно вложенными структурами. Ниже я также публикую частичный javascript для справки, вторая строка функции init является наиболее важной.

Сокращенная часть JavaScript:

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

8 ответов

Решение

Если вы хотите исключить найденный элемент, вы можете использовать нефильтр. Например, я взял функцию, исключающую элемент, и сделал ее короче:

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

Теперь, честно говоря, я не до конца понял, чего ты хочешь. Но когда я взглянул на твою функцию, я увидел, что ты пытался сделать.

В любом случае, посмотрите на эту скрипку, результат такой же, как ваш: http://jsfiddle.net/KX65p/8/

Ну, я действительно не хочу отвечать на мой собственный вопрос о вознаграждении, поэтому, если кто-то может предоставить лучшую или альтернативную реализацию, пожалуйста, сделайте..

Однако, будучи вынужденным завершить проект, я немного поработал над этим и придумал довольно чистый плагин jQuery для выполнения поиска в стиле jQuery.find(), исключая дочерние ветви из результатов по мере продвижения.

Использование для работы с наборами элементов внутри вложенных представлений:

// 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');

Более сложный пример можно найти по адресу http://jsfiddle.net/KX65p/3/ где я использую это для.each() вложенного класса и связывания элементов, которые встречаются в каждом вложенном представлении, с классом. Это позволило мне сделать компоненты на стороне сервера и на стороне клиента отражать свойства друг друга и иметь более дешевую вложенную обработку событий.

Реализация:

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

(Сокращенная версия):

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

Я думаю, что это самый близкий findExclude можно оптимизировать:

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

Кроме того, посмотрите его скрипку с добавленными журналами с истекшим временем в миллисекундах.

Я вижу, что вы обеспокоены выступлениями. Итак, я провел несколько тестов, и эта реализация занимает не более 2 миллисекунд, в то время как ваша реализация (как ответ, который вы опубликовали) иногда занимает около 4-7 миллисекунд.

Может быть, что-то вроде этого будет работать:

$.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/

Выбирает те элементы, которые либо не находятся в родительском элементе маски, либо их ближайший родительский элемент маски совпадает с корневым, либо их ближайший родительский элемент является родителем корневого элемента.

Если я вас понимаю

лучше понимая ваши потребности и применяя конкретные классы, которые вам нужны, я думаю, что этот синтаксис будет работать:

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

Эта Скрипка - попытка воспроизвести ваш сценарий. Не стесняйтесь играть с этим.


Я думаю, что нашел проблему. Вы не включили кнопки в свой not селектор

Я изменил привязку, чтобы быть

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

скрипка

Из моего понимания я бы связал с .controls элементы и позволяют событию пузыриться до них. Из этого можно получить ближайший .Interface чтобы получить родителя, если это необходимо. Таким образом, вы добавляете несколько обработчиков к одним и тем же элементам, когда идете дальше по кроличьей норе.

Хотя я видел, как вы упомянули об этом, я никогда не видел, чтобы это было реализовано.

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

Вот скрипка, показывающая, что я имею в виду. Я действительно удалил ваши JS в пользу упрощенного отображения.

Кажется, что мое решение может быть слишком упрощенным, хотя...


Обновление 2

Итак, вот скрипка, которая определяет некоторые общие функции, которые помогут достичь того, что вы ищете... Я думаю. getInterfaces предоставляет упрощенную функцию для поиска интерфейсов и их элементов управления, при условии, что все интерфейсы всегда имеют элементы управления.

Хотя, возможно, существуют и дополнительные случаи, которые могут подкрасться. Я также чувствую, что должен извиниться, если вы уже пошли по этому пути, а я просто не вижу / не понимаю!


Обновление 3

Хорошо-хорошо. Я думаю, я понимаю, что вы хотите. Вы хотите получить уникальные интерфейсы и иметь набор принадлежащих ему элементов управления, которые имеют смысл сейчас.

Используя эту скрипку в качестве примера, мы выбираем оба .Interface и .Interface .controls,

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

Таким образом, у нас есть аккуратная коллекция интерфейсов и элементов управления, которые принадлежат им в порядке их появления в DOM. При этом мы можем перебрать коллекцию и проверить, есть ли у текущего элемента .Interface связано с этим. Мы также можем сохранить ссылку на текущий интерфейсный объект, который мы создаем для него, чтобы мы могли добавить элементы управления позже.

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

Теперь, когда у нас нет .Interface класс связывается с элементом, мы получили элементы управления. Итак, давайте сначала изменим наш Interface объект для поддержки добавления элементов управления и привязки событий к элементам управления по мере их добавления в коллекцию.

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

Теперь все, что нам нужно сделать, это добавить элемент управления к текущему интерфейсу управления.

currentInterface.addControls(el);

После всего этого вы должны получить массив из 3 объектов (интерфейсов), каждый из которых имеет массив из 2 элементов управления.

Надеюсь, что это все, что вы ищете!

Если бы у ваших классов.interface был какой-то идентификатор, это было бы довольно просто. Perhabs у вас уже есть такой идентификатор по другим причинам или вы хотите включить его.

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" );

Редактировать: И теперь мне интересно, если вы решаете эту проблему под неправильным углом.

Поэтому я пытаюсь $('.Interface'). Each(привязать к любому.controls не внутри более глубокого.Interface)

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

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

Событие будет запущено на.control; затем он будет пузыриться до своего.closest( ".interface"), где он будет обработан и дальнейшее распространение будет остановлено. Разве это не то, что вы описали?

Почему бы не взять проблему с ног на голову?

Выберите все элементы $(. Target), а затем откажитесь от них при дальнейшей обработке, если их.$ Parent (.group) пуст, что даст сонет как:

$('.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
    }
});

Обратите внимание, что не отвечает на заголовок, но буквально дает вам "Селектор B, внутри результата от Селектор А"

Другие вопросы по тегам