JSFiddle - React, Tailwind, and code Playground

Nested Checklist | With Expanding / Collapsing Tree Nodes and Indeterminate Checkboxes

by skibulk

HTML

<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
<div class="nested-checklist">
  <ul>
    <li>
      <fieldset>
        <legend>Colors
          <br>Colours</legend>
        <label class="toggle-select">
          <span>Select?</span>
          <input type="checkbox" id="item-1" checked>
        </label>
        <label class="toggle-expand">
          <span>Expand?</span>
          <input type="checkbox" checked>
        </label>
      </fieldset>
      <ul>
        <li>
          <fieldset>
            <legend>Green</legend>
            <label class="toggle-expand">
              <span>Expand/Collapse</span>
              <input type="checkbox" disabled>
            </label>
            <label class="toggle-select">
              <span>Select/Deselect</span>
              <input type="checkbox" id="item-1">
            </label>
          </fieldset>
        </li>
        <li>
          <fieldset>
            <legend>Red</legend>
            <label class="toggle-expand">
              <span>Expand/Collapse</span>
              <input type="checkbox" disabled>
            </label>
            <label class="toggle-select">
              <span>Select/Deselect</span>
              <input type="checkbox" id="item-1">
            </label>
          </fieldset>
          <ul>
            <li>
              <fieldset>
                <legend>Burgundy</legend>
                <label class="toggle-expand">
                  <span>Expand/Collapse</span>
                  <input type="checkbox" disabled>
                </label>
                <label class="toggle-select">
                  <span>Select/Deselect</span>
                  <input type="checkbox" id="item-1">
                </label>
              </fieldset>
            </li>
            <li>
              <fieldset>
                <legend>Crimson</legend>
                <label class="toggle-expand">
                  <span>Expand/Collapse</span>
                  <input type="checkbox"...

CSS

* {
  box-sizing: border-box;
}

body {
  margin: 20px;
  padding: 0;
  font-family: sans-serif;
}

.nested-checklist {
  font-size: 1rem;
  line-height: 1.4rem;
}

.nested-checklist ul,
.nested-checklist fieldset,
.nested-checklist legend {
  list-style: none;
  margin: 0;
  border: 0;
  padding: 0;
}

.nested-checklist ul ul {
  margin-left: 1rem;
}

.nested-checklist fieldset {
  position: relative;
}

.nested-checklist legend {
  margin-left: 2.3rem;
}

.nested-checklist label {
  position: absolute;
  top: 0;
  width: 1rem;
  height: 1rem;
  background: lightgray;
}

.nested-checklist label.toggle-select {
  left: 1rem;
}

.nested-checklist label span {
  position: absolute;
  clip: rect(1px, 1px, 1px, 1px);
}

.nested-checklist .state-expanded .toggle-expand {
  background-image: url('');
}

.nested-checklist .state-collapsed .toggle-expand {
  background-image: url('');
}

..nested-checklist .state-collapsed ul {
  display: none;
}

.nested-checklist .state-selected .toggle-select {
  background-image: url('');
}

.nested-checklist .state-unselected .toggle-select {
  background-image: url('');
}

.nested-checklist .state-indeterminite .toggle-select {
  background-image: url('');
}

.nested-checklist .state-disabled {
  visibility: hidden;
}

JavaScript

var checkboxSelector = '.toggle-select input';

$(checkboxSelector).change(function(e) {

  var checked = $(this).prop('checked'), // State
    $parents = [].reverse.call($(this).parents('li')), // All Parent List Items
    $parent = $parents.first();
    
	[].shift.call($parents);

  // Apply state to children
  $parent.find(checkboxSelector).prop({
    indeterminate: false,
    checked: checked
  });

  $parents.each(function(index, parent) {

    var $parent = $(parent),
    	$select = $parent.children('> fieldset ' + checkboxSelector),
    	$children = $parent.children('> ul ' + checkboxSelector),
      childrenChecked = $children.prop('checked'),
      indeterminate;

    if (childrenChecked) {

      $select.prop({
        indeterminate: false,
        checked: true
      });

    } else if (childrenChecked && !checked) {

      indeterminate = $parent.find(checkboxSelector + ':checked').length > 0;

      $select.prop({
        indeterminate: indeterminate,
        checked: checked
      });

    } else {

      $select.prop({
        indeterminate: true,
        checked: false
      });

      return false;
    }

  });
});