JSFiddle - React, Tailwind, and code Playground

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

by skibulk

HTML

<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
<ul role="tree" aria-label="Categories" class="nested-checklist">
  <li role="treeitem" aria-labelledby="label-04cd7047"><span id="label-04cd7047">United States</span>
    <input type="checkbox" class="toggle-select" id="select-04cd7047" name="select-04cd7047" checked>
    <label for="select-04cd7047">Enable</label>
    <input type="checkbox" class="toggle-expand" id="expand-04cd7047" checked>
    <label for="expand-04cd7047">Expand</label>
    <ul role="group" aria-label="Subcategories">
      <li role="treeitem" aria-labelledby="label-65ba1118"><span id="label-65ba1118">New York</span>
        <input type="checkbox" class="toggle-select" id="select-65ba1118" name="select-65ba1118" checked>
        <label for="select-65ba1118">Enable</label>
        <input type="checkbox" class="toggle-expand" id="expand-65ba1118" checked>
        <label for="expand-65ba1118">Expand</label>
        <ul role="group" aria-label="Subcategories">
          <li role="treeitem" aria-labelledby="label-65ba1116"><span id="label-65ba1116">Binghamton</span>
            <input type="checkbox" class="toggle-select" id="select-65ba1116" name="select-65ba1116" checked>
            <label for="select-65ba1116">Enable</label>
            <input type="checkbox" class="toggle-expand" id="expand-65ba1116" checked disabled>
            <label for="expand-65ba1116">Expand</label>
          </li>
          <li role="treeitem" aria-labelledby="label-65ba1115"><span id="label-65ba1115">New York</span>
            <input type="checkbox" class="toggle-select" id="select-65ba1115" name="select-65ba1115" checked>
            <label for="select-65ba1115">Enable</label>
            <input type="checkbox" class="toggle-expand" id="expand-65ba1115" checked disabled>
            <label for="expand-65ba1115">Expand</label>
          </li>
        </ul>
      </li>
      <li role="treeitem" aria-labelledby="label-65ba1117"><span...

CSS

body {
  margin: 20px;
  padding: 0;
}

.nested-checklist,
.nested-checklist * {
  position: relative;
  top: 0;
  left: 0;
  box-sizing: border-box;
  margin: 0;
  border: 0;
  padding: 0;
  list-style: none;
  font-family: sans-serif;
  font-size: 1rem;
  line-height: 1.4rem;
  
  cursor: pointer;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  -khtml-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}

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

.nested-checklist ul::before {
  content: '';
  display: block;
  width: 1px;
  
  position: absolute;
  top: 0;
  bottom: .75rem;
  left: -.5rem;
  
  background: #BBB;
}

.nested-checklist span {
  display: inline-block;
  margin-left: 2.3rem;
}

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

.nested-checklist label {
  position: absolute;
  display: inline-block;
  width: 1rem;
  height: 1rem;
  padding-left: 1rem;
  overflow: hidden;
  background: transparent...

JavaScript

// To Do:
// Refactor States
// Vertical lines: Borders instead of Pseudo
// Inverse handler for preformance

$(function() {
  $('.nested-checklist span').click(toggleLabel);
  $('.nested-checklist .toggle-select').change(toggleSelect);

  $('.nested-checklist').each(function(index, tree) {
    updateTree($(tree));
  });
});

function toggleLabel() {

  li_input = $(this).siblings('.toggle-select')[0];
  li_input.checked = !li_input.checked;
  toggleSelect.apply(li_input);

  $(this).parent().find('.toggle-expand').prop('checked', li_input.checked);
}

// Apply checkbox state to nested children
function toggleSelect() {

  $(this).parent().find('.toggle-select').prop({
    indeterminate: false,
    checked: this.checked
  });

  updateTree($(this).parents('.nested-checklist'));
}

function updateTree($ul) {

  var ul_states = {
    true: 0, // Checked
    false: 0 // Unchecked
  };

  $ul.children().each(function(index, li) {

    var li_input = $(li).find('.toggle-select')[0];

    var $ul_ul = $(li).children('ul').first();

    if ($ul_ul.length) {

      var ul_ul_states = updateTree($ul_ul);

      if (ul_ul_states['true'] && ul_ul_states['false']) { // checked and unchecked

        li_input.indeterminate = true;
        li_input.checked = false;

      } else if (ul_ul_states['true'] && !ul_ul_states['false']) { // all checked

        li_input.indeterminate = false;
        li_input.checked = true;

      } else { // all unchecked

        li_input.indeterminate = false;
        li_input.checked = false;
      }

      ul_states[true] += ul_ul_states[true];
      ul_states[false] += ul_ul_states[false];

    } else {

      ul_states[li_input.checked]++;
    }
  });

  return ul_states;
}