Last active
May 27, 2019 22:29
jQuery checkbox tree
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{% macro checkbox_tree(roles, level = 0) %} | |
{% import _self as macros %} | |
<ul{% if level == 0 %} class="roles"{% endif %}> | |
{% for role, child_roles in roles %} | |
<li class="role"> | |
<div class="custom-control custom-checkbox"> | |
<input class="custom-control-input" id="{{ role|lower }}" type="checkbox" name="role[{{ role }}]" | |
data-level="{{ level }}"> | |
<label class="custom-control-label" for="{{ role|lower }}">{{ role }}</label> | |
</div> | |
{% if child_roles is not empty %} | |
{{ macros.checkbox_tree(child_roles, level + 1) }} | |
{% endif %} | |
</li> | |
{% endfor %} | |
</ul> | |
{% endmacro %} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Sort the checkboxes by depth. This is necessary because in updateParentCheckboxes() changes must propagate | |
// upward (since parent checkboxes depend on the states of their descendants). | |
// | |
// [-] root checkbox (indeterminate -> checked) ^ | |
// / \ /|\ | |
// / \ | | |
// / \ | | |
// / \ | | |
// [X] cbx 1 [-] cbx 2 (indeterminate -> checked) | upward | |
// / | | \ | propagation | |
// / | | \ | | |
// / | | \ | | |
// / | | \ | | |
// [X] 1.1 [X] 1.2 [X] 2.1 [ ] 2.2 (2.2 goes checked) | | |
// | |
const $checkboxes = $('.role [type=checkbox]').sort(function toposort(a, b) { | |
return +b.getAttribute('data-level') - (+a.getAttribute('data-level')); | |
}); | |
// Whether indeterminate checkboxes should be checked. | |
const indeterminateMeansChecked = false; | |
// For each checkbox having child checkboxes, accumulate their states and update the parent. | |
// This (un)checks the parent checkbox if all its descendants are (un)checked, or otherwise sets it to | |
// indeterminate state. | |
// | |
// This function traverses the whole checkbox tree whereas in theory it could be limited to a subtree | |
// containing the recently changed checkbox. However, this optimization has not been implemented. | |
function updateParentCheckboxes() { | |
$checkboxes.each(function updateParentCheckbox() { | |
const $this = $(this); | |
const $thisRole = $this.closest('.role'); | |
const $childRoles = $thisRole.find('.role'); | |
// We convert to array because it's easier to use Array#every() this way. | |
const childCheckboxes = $childRoles.find('[type=checkbox]').toArray(); | |
// If this checkbox has no children, do nothing. | |
if (childCheckboxes.length === 0) { | |
return; | |
} | |
if (childCheckboxes.every(x => x.checked)) { | |
// If all child checkboxes are checked, check the parent checkbox. | |
$this.prop('indeterminate', false); | |
$this.prop('checked', true); | |
} else if (childCheckboxes.every(x => !x.checked)) { | |
// Inverse case. | |
$this.prop('indeterminate', false); | |
$this.prop('checked', false); | |
} else { | |
// Some but not all child checkboxes checked. The parent is in indeterminate state. | |
$this.prop('checked', indeterminateMeansChecked); | |
$this.prop('indeterminate', true); | |
} | |
}); | |
} | |
// If there are child checkboxes, set them all to the same state as the parent, recursively. | |
function propagateToChildCheckboxes($parent) { | |
const $parentRole = $parent.closest('.role'); | |
const $descendants = $parentRole.find('[type=checkbox]'); | |
$descendants.prop('checked', $parent.prop('checked')); | |
} | |
$checkboxes.change(function handleChangedCheckbox() { | |
propagateToChildCheckboxes($(this)); | |
updateParentCheckboxes(); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment