Отметить все чекбоксы родительского уровня при отметке дочернего

Самописный движок, куча чекбоксов выводится следующим образом:

<input type="checkbox" data-current="101" data-parent="100" data-level="1" name="checkboxes[]" value="NNN">
<input type="checkbox" data-current="256" data-parent="101" data-level="2" name="checkboxes[]" value="NNN">
<input type="checkbox" data-current="1234" data-parent="256" data-level="3" name="checkboxes[]" value="NNN">
<input type="checkbox" data-current="1236" data-parent="256" data-level="3" name="checkboxes[]" value="NNN">
<input type="checkbox" data-current="2274" data-parent="1236" data-level="4" name="checkboxes[]" value="NNN">
<input type="checkbox" data-current="257" data-parent="101" data-level="2" name="checkboxes[]" value="NNN">
<input type="checkbox" data-current="7434" data-parent="257" data-level="3" name="checkboxes[]" value="NNN">
<input type="checkbox" data-current="102" data-parent="100" data-level="1" name="checkboxes[]" value="NNN">
<input type="checkbox" data-current="336" data-parent="102" data-level="2" name="checkboxes[]" value="NNN">
<input type="checkbox" data-current="7458" data-parent="336" data-level="3" name="checkboxes[]" value="NNN">
<input type="checkbox" data-current="9800" data-parent="336" data-level="3" name="checkboxes[]" value="NNN">

Видно, что у списка чекбоксов соблюдается структура. Есть чекбоксы 1-го уровня data-level="1", второго и тд. Также у каждого чекбокса есть его айди data-current, и айди родителя data-parent.
Нужно сделать так, чтобы при отметке любого чекбокса, автоматически отмечался его родитель, а также родители родителя, если они есть. Желательно также, чтобы при снятии отметки с родителя, снимались все отметки с его дочерних.
Спасибо.


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

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

Предлагаю воспользоваться делегацией события (это сильно снизит нагрузку, т.к. не будем на каждый input вешать слушатель):

  1. Оборачиваем все инпуты в один большой контейнер. Обёртка в дополнительный label был сделан чисто для удобства стилизации

  2. Вешаем слушателя на событие клик (inputsContainer.addEventListener('click') на, обёртывающий все инпуты, контейнер

  3. Далее убеждаемся что мы нажали именно на наш checkbox (target.type !== 'checkbox')

  4. Далее в зависимости от значения checkbox-а (newState) выполняем один из двух действий (action):

    • Если мы выбрали, то запускаем функцию (activateAllParents), которая найдёт все нужные родительские элементы и выберет их

    • Если мы сняли выбор, то запускаем функцию (deactivateAllChildren), которая найдёт все нужные дочерние элементы и у всех снимет выбор

Теперь разберёмся в том как работают наши функции activateAllParents и deactivateAllChildren

Начнём с activateAllParents:

  1. На вход ожидается нажатый checkbox, от которого мы и будем отталкиваться

  2. Находим его родителя, с помощью функции getParent

    2.1. По данной структуре, родителем для элемента считается тот, у кого аттрибут data-current равен аттрибуту data-parent, у самого элемента. Следовательно нужно достать значение аттрибута data-parent у самого элемента (child.dataset.parent) и сделать поиск внутри контейнера (inputsContainer.querySelector) и найти того, у кого в data-current написано то же самое ([data-current="${child.dataset.parent}"]). Выбран метод querySelector потому что у элемента, не может больше одного родителя

  3. Далее запускаем цикл (while), который запускает ряд дейтсвий до тех пор пока родитель находится (parent !== null):

    3.1. Если родитль уже выбран (parent.checked === true), то нет смысла идти дальше вверх по цепочке, потому можно завершить работу функции

    3.2. Выбираем родителя (parent.checked = true)

    3.3. Ищем родителя родителя (parent = getParent(parent))

Теперь рассмотрим deactivateAllChildren:

  1. На вход ожидается нажатый checkbox, от которого мы и будем отталкиваться

  2. Находим все его дочерние элементы, с помощью функции getAllChildren

    2.1. По данной структуре, дочерними для элемента считаются те, у которых аттрибут data-parent равен аттрибуту data-current, у самого элемента. Следовательно нужно достать значение аттрибута data-current у самого элемента (parent.dataset.current) и сделать поиск внутри контейнера (inputsContainer.querySelectorAll) и найти тех, у которых в data-parent написано то же самое ([data-parent="${parent.dataset.current}"]). Выбран метод querySelectorAll потому что у элемента, может больше одного дочернего элемента

  3. Проходимся по всем дочерним элементам children.forEach

    3.1. Если дочерний элемент и так уже не выбран (child.checked === false), то нет смысла идти дальше вглубь, потому можно завершить работу для этого дочернего элемента

    3.2. Убираем выбор у дочернего (child.checked = false)

    3.3. Рекурсивно убираем выбор у всех дочерних элементов данного дочернего элемента (deactivateAllChildren(child))

Про dataset можете прочитать тут

Конечный результат:

const inputsContainer = document.querySelector('.inputs');

const getParent = child => inputsContainer.querySelector(`[data-current="${child.dataset.parent}"]`);
const getAllChildren = parent => inputsContainer.querySelectorAll(`[data-parent="${parent.dataset.current}"]`);

const activateAllParents = child => {
  let parent = getParent(child);

  while (parent !== null) {
    if (parent.checked === true) return;
    
    parent.checked = true;

    parent = getParent(parent);
  }
}

const deactivateAllChildren = parent => {
  const children = getAllChildren(parent);

  children.forEach(child => {
    if (child.checked === false) return;
  
    child.checked = false;

    deactivateAllChildren(child);
  })
}

inputsContainer.addEventListener('click', e => {
  const target = e.target;

  if (target.type !== 'checkbox') return;

  const newState = target.checked;
  const action = newState ? activateAllParents : deactivateAllChildren;

  action(target);
})
.inputs {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

label {
  cursor: pointer;
}
<div class="inputs">
  <label>
    <input type="checkbox" data-current="101" data-parent="100" data-level="1" name="checkboxes[]" value="NNN">1
  </label>
  <label>
    <input type="checkbox" data-current="256" data-parent="101" data-level="2" name="checkboxes[]" value="NNN">1.1
  </label>
  <label>
    <input type="checkbox" data-current="1234" data-parent="256" data-level="3" name="checkboxes[]" value="NNN">1.1.1
  </label>
  <label>
    <input type="checkbox" data-current="1236" data-parent="256" data-level="3" name="checkboxes[]" value="NNN">1.1.2
  </label>
  <label>
    <input type="checkbox" data-current="2274" data-parent="1236" data-level="4" name="checkboxes[]" value="NNN">1.1.2.1
  </label>
  <label>
    <input type="checkbox" data-current="257" data-parent="101" data-level="2" name="checkboxes[]" value="NNN">1.2
  </label>
  <label>
    <input type="checkbox" data-current="7434" data-parent="257" data-level="3" name="checkboxes[]" value="NNN">1.2.1
  </label>
  <label>
    <input type="checkbox" data-current="102" data-parent="100" data-level="1" name="checkboxes[]" value="NNN">2
  </label>
  <label>
    <input type="checkbox" data-current="336" data-parent="102" data-level="2" name="checkboxes[]" value="NNN">2.1
  </label>
  <label>
    <input type="checkbox" data-current="7458" data-parent="336" data-level="3" name="checkboxes[]" value="NNN">2.1.1
  </label>
  <label>
    <input type="checkbox" data-current="9800" data-parent="336" data-level="3" name="checkboxes[]" value="NNN">2.1.2
  </label>
</div>

→ Ссылка