Похоже на jQuery .closest (), но пересекает потомков?

123

Есть ли функция, аналогичная, jQuery .closest()но для обхода потомков и возврата только самых близких?

Я знаю, что .find()функция есть, но она возвращает все возможные совпадения, а не самые близкие.

Редактировать:

Вот определение ближайшего (по крайней мере, для меня) :

Сначала пересекаются все дети, затем пересекаются отдельные дети.

В приведенном ниже примере id='2'- ближайшие .closestпотомкиid="find-my-closest-descendant"

<div id="find-my-closest-descendant">
    <div>
        <div class="closest" Id='1'></div>
    </div>
    <div class="closest" Id='2'></div>
</div>

См. Ссылку JSfiddle .

Маму
источник
4
О чем .find("filter here").eq(0)?
Shadow Wizard is Ear For You
@ShadowWizard хорошо, если он выполняет обход в глубину, тогда нулевой элемент в списке результатов может не быть «ближайшим», в зависимости от того, что означает «ближайший».
Pointy
@Pointy, разве это не :firstфильтр?
Shadow Wizard is Ear For You
Нет, я так не думаю - проблема в том, что «: first» и «.eq (0)» относятся к порядку DOM, но если «ближайший» означает «наименьшее количество узлов DOM», тогда «внук» первый дочерний элемент будет возвращен вместо более позднего «ребенка», который соответствует селектору. Конечно, я не уверен на 100%, что это то, о чем идет речь.
Pointy
1
Есть плагин jQuery, который именно это делает для вас: plugins.jquery.com/closestDescendant
TLindig

Ответы:

44

Согласно вашему определению closest, я написал следующий плагин:

(function($) {
    $.fn.closest_descendent = function(filter) {
        var $found = $(),
            $currentSet = this; // Current place
        while ($currentSet.length) {
            $found = $currentSet.filter(filter);
            if ($found.length) break;  // At least one match: break loop
            // Get all children of the current set
            $currentSet = $currentSet.children();
        }
        return $found.first(); // Return first match of the collection
    }    
})(jQuery);
Роб В
источник
3
В отличие от .closest()функции jQuery , она также соответствует элементу, к которому она была вызвана. См. Мой jsfiddle . При изменении на $currentSet = this.children(); // Current placeэто начнется с его дочерними
элементами
21
JQuery closest () также может быть сопоставлен с текущим элементом.
Давид Хорват,
120

Если под «ближайшим» потомком вы имеете в виду первого ребенка, вы можете:

$('#foo').find(':first');

Или:

$('#foo').children().first();

Или, чтобы найти первое появление определенного элемента, вы можете сделать:

$('#foo').find('.whatever').first();

Или:

$('#foo').find('.whatever:first');

На самом деле нам нужно твердое определение того, что означает «ближайший потомок».

Например

<div id="foo">
    <p>
        <span></span>
    </p>
    <span></span>
</div>

Что <span>бы $('#foo').closestDescendent('span')вернуться?

Джеймс
источник
1
спасибо @ 999 за подробный ответ, мое ожидание умершего - сначала пройдены все дети, а затем все дети. В приведенном вами примере будет возвращен второй диапазон
mamu
8
Так что сначала дыхание, а не глубина
jessehouwing
1
Отличный ответ. Мне было бы интересно узнать, $('#foo').find('.whatever').first();«в ширину» или «в глубину»
Колин
1
Единственная проблема с этим решением заключается в том, что jQuery найдет ВСЕ критерии соответствия, а затем вернет первый. Хотя движок Sizzle работает быстро, это представляет собой ненужный дополнительный поиск. Невозможно прервать поиск и остановить поиск после обнаружения первого совпадения.
icfantv 02
Все эти примеры выбирают потомков в глубину, а не в ширину, поэтому их нельзя использовать для решения исходного вопроса. В моем тестировании все они вернули первый диапазон, а не второй.
JrBaconCheez
18

Вы можете использовать findс :firstселектором:

$('#parent').find('p:first');

Вышеупомянутая строка найдет первый <p>элемент в потомках #parent.

FishBasketGordo
источник
2
Я думаю, что это то, что хотел OP, плагин не требуется. Это выберет первый соответствующий элемент в потомках выбранного элемента для каждого выбранного элемента. Таким образом, если у вас есть 4 совпадения с вашим исходным выбором, вы можете получить 4 совпадения, используя расширение find('whatever:first').
RustyToms
3

А как насчет этого подхода?

$('find-my-closest-descendant').find('> div');

Этот "прямой дочерний" селектор у меня работает.

klawipo
источник
ОП специально просит что-то, что пересекает потомков, чего в этом ответе нет. Это может быть хороший ответ, но не для этого конкретного вопроса.
jumps4fun
@KjetilNordin Может быть, это потому, что вопрос был отредактирован после моего ответа. Более того, определение ближайшего потомка все еще не так ясно, по крайней мере, для меня.
klawipo
Я однозначно согласен. ближайший потомок мог означать очень многое. Проще с родителем в этом случае, когда для элементов всегда есть единственный родитель на каждом уровне. И ты прав насчет возможных правок. Теперь я вижу, что мой комментарий никоим образом не помог, тем более, что уже было утверждение того же типа.
jumps4fun
1

Чистое решение JS (с использованием ES6).

export function closestDescendant(root, selector) {
  const elements = [root];
  let e;
  do { e = elements.shift(); } while (!e.matches(selector) && elements.push(...e.children));
  return e.matches(selector) ? e : null;
}

пример

Учитывая следующую структуру:

div == $ 0
├── div == $ 1
│ ├── div
│ ├── div.findme == $ 4
│ ├── div
│ └── div
├── div.findme == $ 2
│ ├── div
│ └── div
└── div == $ 3
    ├── div
    ├── div
    └── div
closestDescendant($0, '.findme') === $2;
closestDescendant($1, '.findme') === $4;
closestDescendant($2, '.findme') === $2;
closestDescendant($3, '.findme') === null;

ccjmne
источник
Это решение работает, но я разместил здесь альтернативное решение ES6 ( stackoverflow.com/a/55144581/2441655 ), поскольку мне не нравятся: 1) whileВыражение с побочными эффектами. 2) Использование .matchesчека на двух строках вместо одной.
Venryx
1

Если кто-то ищет чистое JS-решение (используя ES6 вместо jQuery), вот то, которое я использую:

Element.prototype.QuerySelector_BreadthFirst = function(selector) {
    let currentLayerElements = [...this.childNodes];
    while (currentLayerElements.length) {
        let firstMatchInLayer = currentLayerElements.find(a=>a.matches && a.matches(selector));
        if (firstMatchInLayer) return firstMatchInLayer;
        currentLayerElements = currentLayerElements.reduce((acc, item)=>acc.concat([...item.childNodes]), []);
    }
    return null;
};
Venryx
источник
Привет, спасибо, что обратили мое внимание на свой ответ! Вы правы, оглядываясь на мою собственную, мне кажется, что она пытается быть слишком умной и на самом деле неудобной ( matchesдважды бегать тоже меня немного раздражает). +1 для вас :)
ccjmne
0

Я думаю, что сначала нужно определить, что означает «ближайший». Если вы имеете в виду подчиненный узел, соответствующий вашим критериям, который является кратчайшим расстоянием с точки зрения родительских ссылок, то использование ": first" или ".eq (0)" не обязательно будет работать:

<div id='start'>
  <div>
    <div>
      <span class='target'></span>
    </div>
  </div>
  <div>
    <span class='target'></span>
  </div>
</div>

В этом примере второй <span>элемент «.target» находится «ближе» к «началу» <div>, потому что до него всего один родительский переход. Если это то, что вы подразумеваете под «ближайшим», вам нужно будет найти минимальное расстояние в функции фильтра. Список результатов из селектора jQuery всегда находится в порядке DOM.

Может быть:

$.fn.closestDescendant = function(sel) {
  var rv = $();
  this.each(function() {
    var base = this, $base = $(base), $found = $base.find(sel);
    var dist = null, closest = null;
    $found.each(function() {
      var $parents = $(this).parents();
      for (var i = 0; i < $parents.length; ++i)
        if ($parents.get(i) === base) break;
      if (dist === null || i < dist) {
        dist = i;
        closest = this;
      }
    });
    rv.add(closest);
  });
  return rv;
};

Это своего рода хакерский плагин из-за того, как он создает объект результата, но идея состоит в том, что вам нужно найти кратчайший родительский путь из всех совпадающих элементов, которые вы найдете. Этот код смещается в сторону элементов влево в дереве DOM из-за <проверки; <=будет смещаться вправо.

Заостренный
источник
0

Я приготовил это, пока нет реализации для позиционных селекторов (их нужно больше, чем просто matchesSelector):

Демо: http://jsfiddle.net/TL4Bq/3/

(function ($) {
    var matchesSelector = jQuery.find.matchesSelector;
    $.fn.closestDescendant = function (selector) {
        var queue, open, cur, ret = [];
        this.each(function () {
            queue = [this];
            open = [];
            while (queue.length) {
                cur = queue.shift();
                if (!cur || cur.nodeType !== 1) {
                    continue;
                }
                if (matchesSelector(cur, selector)) {
                    ret.push(cur);
                    return;
                }
                open.unshift.apply(open, $(cur).children().toArray());
                if (!queue.length) {
                    queue.unshift.apply(queue, open);
                    open = [];
                }
            }
        });
        ret = ret.length > 1 ? jQuery.unique(ret) : ret;
        return this.pushStack(ret, "closestDescendant", selector);
    };
})(jQuery);

Хотя, вероятно, есть некоторые ошибки, не очень много тестировал.

Esailija
источник
0

Ответ Роба У меня не совсем сработал. Я адаптировал его к этому, и это сработало.

//closest_descendent plugin
$.fn.closest_descendent = function(filter) {
    var found = [];

    //go through every matched element that is a child of the target element
    $(this).find(filter).each(function(){
        //when a match is found, add it to the list
        found.push($(this));
    });

    return found[0]; // Return first match in the list
}
Дэниел Тонон
источник
Для меня это выглядит именно так .find(filter).first(), только медленнее;)
Маттиас Самсель
Да ты прав. Я написал это, когда еще был новичком в программировании. Думаю, я удалю этот ответ
Дэниел Тонон
0

Я бы использовал следующее, чтобы включить саму цель, если она соответствует селектору:

    var jTarget = $("#doo");
    var sel = '.pou';
    var jDom = jTarget.find(sel).addBack(sel).first();

Разметка:

<div id="doo" class="pou">
    poo
    <div class="foo">foo</div>
    <div class="pou">pooo</div>
</div>
вереск
источник
0

Несмотря на то, что это старая тема, я не смог удержаться от реализации моего closestChild. Доставляет первого найденного потомка с наименьшим количеством путешествий (сначала дыхание). Один рекурсивный (личный фаворит), другой использует список задач без рекурсии в качестве расширений jQquery.

Надеюсь, кому-то это выгодно.

Примечание: рекурсивный метод вызывает переполнение стека, и я улучшил другой, теперь аналогичный предыдущему приведенному ответу.

jQuery.fn.extend( {

    closestChild_err : function( selector ) { // recursive, stack overflow when not found
        var found = this.children( selector ).first();
        if ( found.length == 0 ) {
            found = this.children().closestChild( selector ).first(); // check all children
        }
        return found;
    },

    closestChild : function( selector ) {
        var todo = this.children(); // start whith children, excluding this
        while ( todo.length > 0 ) {
            var found = todo.filter( selector );
            if ( found.length > 0 ) { // found closest: happy
                return found.first();
            } else {
                todo = todo.children();
            }
        }
        return $();
    },

});  
Eelco
источник
0

Следующий плагин возвращает n ближайших потомков.

$.fn.getNthClosestDescendants = function(n, type) {
  var closestMatches = [];
  var children = this.children();

  recursiveMatch(children);

  function recursiveMatch(children) {
    var matches = children.filter(type);

    if (
      matches.length &&
      closestMatches.length < n
    ) {
      var neededMatches = n - closestMatches.length;
      var matchesToAdd = matches.slice(0, neededMatches);
      matchesToAdd.each(function() {
        closestMatches.push(this);
      });
    }

    if (closestMatches.length < n) {
      var newChildren = children.children();
      recursiveMatch(newChildren);
    }
  }

  return closestMatches;
};
капуста
источник
0

Я искал аналогичное решение (мне нужны были все ближайшие потомки, т.е. сначала ширина + все совпадения, независимо от того, на каком уровне оно существует), вот что я в итоге сделал:

var item = $('#find-my-closest-descendant');
item.find(".matching-descendant").filter(function () {
    var $this = $(this);
    return $this.parent().closest("#find-my-closest-descendant").is(item);
}).each(function () {
    // Do what you want here
});

Надеюсь, это поможет.

Шарике Абдулла
источник
0

Вы можете просто сказать:

$("#find-my-closest-descendant").siblings('.closest:first');
Джиоти ватпаде
источник
0

Есть отличная статья, в которой указывается, что то, что требует OP, может быть легко достигнуто с помощью ближайших [Компьютерщиков для компьютерных фанатов]
1 https://www.geeksforgeeks.org/jquery-closest-with-examples/

Жан Поль AKA el_vete
источник