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;
}