Схожий на 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 - це вухо для вас

@ShadowWizard добре, якщо він здійснює проходження глибини першим, то нульовий елемент у списку результатів може бути не "найближчим", залежно від того, що означає "найближчий".
Pointy

@Pointy Це не те саме, що :firstфільтр?
Shadow Wizard є Ear for You

Ні, я не вважаю так - проблема полягає в тому, що ": first" і ".eq (0)" посилаються на порядок DOM, але якщо "найближчий" означає "найменше вузлів DOM далеко", то "онук" Перша дитина буде повернута замість пізнішої "дитини", яка відповідає селектору. Я не на 100% впевнений, що це, звичайно, ОП.
Поні

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нього, почнемо це саме діти, а не jsfiddle
allicarn

21
Найближчий () JQuery також може відповідати поточному елементу.
Давид Хорват

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 за детальну відповідь, моє очікування спадкодавця спочатку проходять усі дитячі, а потім кожен дитячий. У прикладі, який ви подали, буде повернуто другий проміжок
мама

8
Тож дихання перше, а не глибина перше
jessehouwing

1
Відмінна відповідь. Мені було б цікаво дізнатись, чи $('#foo').find('.whatever').first();є "широта перша" чи "перша глибина"
Колін

1
Одна з проблем цього рішення полягає в тому, що jQuery знайде ВСІ відповідні критерії та поверне перший. Незважаючи на те, що двигун Sizzle працює швидко, це означає непотрібні додаткові пошуки. Немає способу короткого замикання пошуку та зупинки після того, як буде знайдено перший матч.
icfantv

Усі ці приклади вибирають нащадків першої глибини, а не першої ширини, тому їх не можна використовувати для вирішення початкового питання. У моєму тестуванні всі вони повернули перший проміжок, а не другий.
JrBaconCheez

18

Ви можете використовувати findразом із :firstселектором:

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

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


2
Я думаю, що це те, чого хотів ОП, плагін не потрібен. Це вибере перший відповідний елемент у нащадках вибраного елемента для кожного обраного елемента. Тож якщо у вас є 4 матчі з оригінальним вибором, ви можете закінчити 4 матчі за допомогою find('whatever:first').
RustyToms

3

Що з таким підходом?

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

Цей селектор "прямий дитина" працює на мене.


ОП спеціально просять щось, що обходить нащадків, чого ця відповідь не відповідає. Це може бути хорошою відповіддю, але не для цього конкретного питання.
стрибки4фун

@KjetilNordin Можливо, це питання було відредаговано з моєї відповіді. Більше того, визначення найближчого нащадка все ще не настільки чітке, принаймні, для мене.
klawipo

Я однозначно згоден. найближчий нащадок може означати стільки речей. Простіше з батьком у цьому випадку, коли на кожному рівні завжди є один батьківський елемент. І ви маєте рацію щодо можливих редагувань. Зараз я бачу, що мій коментар не допомагав жодним чином, тим більше, що там вже було повідомлення одного типу.
стрибки4фун

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.findme == 4 $
│ ├── розд
│ └── розд
├── div.findme == $ 2
│ ├── розд
│ └── розд
└── div == 3 $
    ├── див
    ├── див
    └── див
closestDescendant($0, '.findme') === $2;
closestDescendant($1, '.findme') === $4;
closestDescendant($2, '.findme') === $2;
closestDescendant($3, '.findme') === null;


Це рішення працює, але я відправив альтернативне рішення ES6 тут ( stackoverflow.com/a/55144581/2441655 ) , так як мені не подобається: 1) whileвираз , що має побічні ефекти. 2) Використання .matchesчека в двох рядках, а не в одному.
Венрікс

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

Гей, дякую за те, що ти звернув увагу на мою увагу! Ти маєш рацію, озираючись на моє власне, здається, що намагається бути занадто розумним і насправді незручним (біг 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);

Напевно, є якісь помилки, але не дуже тестували.


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

Незважаючи на те, що це стара тема, я не втримався від реалізації своєї найближчої дитини. Поставляє першого знайденого нащадка з найменшими подорожами (перший подих). Один є рекурсивним (особистий фаворит), інший використовує список todo, так що без рекурсії як розширення 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 $();
    },

});  

0

Наступний плагін повертає п ятих найближчих нащадків.

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

Я сподіваюся, що це допомагає.




Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.