Скрыть элемент, если у всех потомков есть класс

Всех приветствую. Задача такая:

Есть input (js-city-search), в который вводится текст. Если содержимое input'а не находит совпадений в текстовом содержимом элемента (js-map-cities__item), то элементу (js-map-cities__item) добавляется класс "hidden", который скрывает его. Если абсолютно все элементы (js-map-cities__item) внутри своего (js-spoiler-body) имеют класс "hidden", то ближайший выше по уровню предок (js-spoiler) тоже должен получать класс "hidden". Если хотя бы 1 js-map-cities__item будет без класса "hidden", то и js-spoiler его получать не должен.

В прикрепленном мною коде есть функционал скрытия js-map-cities__item. Но для решения остальной части задачи у меня знаний не хватает.

Буду благодарен за вашу помощь.

$(document).on('keyup', '.js-city-search', function keyupInput() {
    const $inp = $(this);
    const $inpValue = $inp.val();
    const $items = $inp.closest('.map-cities').find('.js-search-body .js-map-cities__item');

    $items.each(function citySearch(index, value) {
      if (!value.innerHTML.toLowerCase().includes($inpValue.toLowerCase())) { // !== $inpValue
        $(this).addClass('hidden');
      } else {
        $(this).removeClass('hidden');
      }
    });
  });
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<div class="map-cities">
  <div class="map-cities__search">
    <input type="search" name="" id="" placeholder="Поиск по региону" class="inp js-city-search" />
  </div>
  <div class="map-cities__body js-search-body">
    <div class="map-cities__spoiler js-spoiler">
      <a href="#" data-city-id="4" class="map-cities__spoiler-toggler js-toggle-spoiler">
        <span>Белгородская область</span>
        <span class="counter">(34)</span>
      </a>
      <div class="map-cities__spoiler-body js-spoiler-body">
        <a href="#" data-balloon-id="461" class="map-cities__item js-map-cities__item">
          <b>Белгородская область</b>, Николаево, улица Простая, 75<br />
        </a>
        <a href="#" data-balloon-id="462" class="map-cities__item js-map-cities__item">
          <b>Белгородская область</b>, Петрово, улица Короткая, 3<br />
        </a>
        <a href="#" data-balloon-id="439" class="map-cities__item js-map-cities__item">
          <b>Белгородская область</b>, Павловка, улица Советская, 2<br />
        </a>
      </div>
    </div>
  </div>
</div>


Ответы (2 шт):

Автор решения: ksa

Есть input (js-city-search), в который вводится текст. Если содержимое input'а не находит совпадений в текстовом содержимом элемента (js-map-cities__item), то элементу (js-map-cities__item) добавляется класс "hidden", который скрывает его. Если абсолютно все элементы (js-map-cities__item) внутри своего (js-spoiler-body) имеют класс "hidden", то ближайший выше по уровню предок (js-spoiler) тоже должен получать класс "hidden".

Предложу такой вариант решения такой задачи...

let t
$('.map-cities__search input').on('input', function(){
  if (t) clearTimeout(t)
  const v = this.value.toLowerCase()
  t = setTimeout(test, 100, v)
});
//
function test(v){
  $('.map-cities .off').removeClass('off')
  if (v == '') return
  $('.map-cities .map-cities__item').filter((_, o) => !o.textContent.toLowerCase().includes(v)).addClass('off')
  $('.map-cities .js-spoiler:not(:has(.map-cities__item:not(.off)))').addClass('off')
}
.off {
  display: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<div class="map-cities">
  <div class="map-cities__search">
    <input type="search" name="" id="" placeholder="Поиск по региону" class="inp js-city-search" />
  </div>
  <div class="map-cities__body js-search-body">
    <div class="map-cities__spoiler js-spoiler">
      <a href="#" data-city-id="4" class="map-cities__spoiler-toggler js-toggle-spoiler">
        <span>Белгородская область</span>
        <span class="counter">(34)</span>
      </a>
      <div class="map-cities__spoiler-body js-spoiler-body">
        <a href="#" data-balloon-id="461" class="map-cities__item js-map-cities__item">
          <b>Белгородская область</b>, Николаево, улица Простая, 75<br />
        </a>
        <a href="#" data-balloon-id="462" class="map-cities__item js-map-cities__item">
          <b>Белгородская область</b>, Петрово, улица Короткая, 3<br />
        </a>
        <a href="#" data-balloon-id="439" class="map-cities__item js-map-cities__item">
          <b>Белгородская область</b>, Павловка, улица Советская, 2<br />
        </a>
      </div>
    </div>
  </div>
</div>

→ Ссылка
Автор решения: yar85

С минимальными правками примера и логики решения:

$(document).on('keyup', '.js-city-search', function keyupInput() {
  const $inp = $(this),
        inpValue = $inp.val().trim().toLowerCase(),
        $spoilers = $inp.closest('.map-cities').find('.js-search-body .js-spoiler');
  $spoilers.each(function (index, sp) {
    const $spoiler = $(sp),
          $items = $spoiler.find('.js-map-cities__item');
    $items.each(function citySearch(index, el) {
      const elText = el.textContent.trim().toLowerCase();
      $(el).toggleClass('hidden', !elText.includes(inpValue));
    });
    const notHiddenCnt = $spoiler.has('.js-map-cities__item:not(.hidden)').length;
    $spoiler.toggleClass('hidden', !notHiddenCnt);
  });
});
.hidden { display: none; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<div class="map-cities">
  <div class="map-cities__search">
    <input type="search" name="" id="" placeholder="Поиск по региону" class="inp js-city-search" />
  </div>
  <div class="map-cities__body js-search-body">
    <div class="map-cities__spoiler js-spoiler">
      <a href="#" data-city-id="4" class="map-cities__spoiler-toggler js-toggle-spoiler">
        <span>Белгородская область</span>
        <span class="counter">(34)</span>
      </a>
      <div class="map-cities__spoiler-body js-spoiler-body">
        <a href="#" data-balloon-id="461" class="map-cities__item js-map-cities__item">
          <b>Белгородская область</b>, Николаево, улица Простая, 75<br />
        </a>
        <a href="#" data-balloon-id="462" class="map-cities__item js-map-cities__item">
          <b>Белгородская область</b>, Петрово, улица Короткая, 3<br />
        </a>
        <a href="#" data-balloon-id="439" class="map-cities__item js-map-cities__item">
          <b>Белгородская область</b>, Павловка, улица Советская, 2<br />
        </a>
      </div>
    </div>
  </div>
</div>

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

Это неоптимальное, но самое простое решение задачи.
В качестве оптимизаций, лучше будет выполнять поиск по однократно сформированному словарю "текстЭлемента: элемент" исключив основную массу повторяемых действий, а также добавить в обработчик событий разрядку их частоты (паттерн debounce можно использовать, например).

→ Ссылка