Вернуться к уроку

Раскрывающееся дерево

важность: 5

Создайте дерево, которое по клику на заголовок скрывает-показывает детей:

Требования:

  • Использовать делегирование.
  • Клик вне текста заголовка (на пустом месте) ничего делать не должен.
  • При наведении на заголовок – он становится жирным, реализовать через CSS.

P.S. При необходимости HTML/CSS дерева можно изменить.

Открыть песочницу для задачи.

Схема решения

Дерево устроено как вложенный список.

Клики на все элементы можно поймать, повесив единый обработчик onclick на внешний UL.

Как поймать клик на заголовке? Элемент LI является блочным, поэтому нельзя понять, был ли клик на тексте, или справа от него.

Например, ниже – участок дерева с выделенными рамкой узлами. Кликните справа от любого заголовка. Видите, клик ловится? А лучше бы такие клики (не на тексте) игнорировать.

<style>
  li {
    border: 1px solid green;
  }
</style>

<ul onclick="alert(event.target)">
  <li>Млекопетающие
    <ul>
      <li>Коровы</li>
      <li>Ослы</li>
      <li>Собаки</li>
      <li>Тигры</li>
    </ul>
  </li>
</ul>

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

Один из способов это поправить – обернуть заголовки в дополнительный элемент SPAN, и обрабатывать только клики внутри SPAN'ов, получать по SPAN'у его родителя LI и ставить ему класс открыт/закрыт.

Напишите для этого JavaScript-код.

Оборачиваем заголовки в SPAN

Следующий код ищет все LI и оборачивает текстовые узлы в SPAN.

var treeUl = document.getElementsByTagName('ul')[0];

var treeLis = treeUl.getElementsByTagName('li');

for (var i = 0; i < treeLis.length; i++) {
  var li = treeLis[i];

  var span = document.createElement('span');
  li.insertBefore(span, li.firstChild); // добавить пустой SPAN
  span.appendChild(span.nextSibling); // переместить в него заголовок
}

Теперь можно отслеживать клики на заголовках.

Так выглядит дерево с обёрнутыми в SPAN заголовками и делегированием:

<style>
  span {
    border: 1px solid red;
  }
</style>

<ul onclick="alert(event.target.tagName)">
  <li><span>Млекопетающие</span>
    <ul>
      <li><span>Коровы</span></li>
      <li><span>Ослы</span></li>
      <li><span>Собаки</span></li>
      <li><span>Тигры</span></li>
    </ul>
  </li>
</ul>

Так как SPAN – инлайновый элемент, он всегда такого же размера как текст. Да здравствует SPAN!

В реальной жизни дерево, скорее всего, будет сразу со SPAN: если HTML-код дерева генерируется на сервере, то это несложно, если дерево генерируется в JavaScript – тем более просто.

Итоговое решение

Для делегирования нужно по клику понять, на каком узле он произошел.

В нашем случае у SPAN нет детей-элементов, поэтому не нужно подниматься вверх по цепочке родителей. Достаточно просто проверить event.target.tagName == 'SPAN', чтобы понять, где был клик, и спрятать потомков.

var tree = document.getElementsByTagName('ul')[0];

tree.onclick = function(event) {
  var target = event.target;

  if (target.tagName != 'SPAN') {
    return; // клик был не на заголовке
  }

  var li = target.parentNode; // получить родительский LI

  // получить UL с потомками -- это первый UL внутри LI
  var childrenContainer = li.getElementsByTagName('ul')[0];

  if (!childrenContainer) return; // потомков нет -- ничего не надо делать

  // спрятать/показать (можно и через CSS-класс)
  childrenContainer.hidden = !childrenContainer.hidden;
}

Выделение узлов жирным при наведении делается при помощи CSS-селектора :hover.

Открыть решение в песочнице.