Пропустить рекурсию в 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;
}
Выбирает те элементы, которые либо не находятся в родительском элементе маски, либо их ближайший родительский элемент маски совпадает с корневым, либо их ближайший родительский элемент является родителем корневого элемента.
Если я вас понимаю
лучше понимая ваши потребности и применяя конкретные классы, которые вам нужны, я думаю, что этот синтаксис будет работать:
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 у вас уже есть такой идентификатор по другим причинам или вы хотите включить его.
<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)
$(".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, внутри результата от Селектор А"