-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathindeterminateCheckbox.js
217 lines (184 loc) · 9.11 KB
/
indeterminateCheckbox.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
(function() {
// Configs:
var selectorForCheckboxes = 'input[type="checkbox"].indeterminate-checkbox',
stepsFromCheckboxToContainer = 1,
stepsFromContainerToParentContainer = 2;
var IndeterminateCheckbox = {
init: function() {
// Hook the onchange event for all checkboxes of the appropriate class
$(selectorForCheckboxes).change(function(e) {
var checkStateOfTarget = $(this).prop("checked"),
container = getListItemContainingCheckbox($(this));
markAnyChildrenToShareTheCheckStateOfTarget(container, checkStateOfTarget);
checkRelatives(container);
/**
* This recursion based function will check if the siblings of el
* all match the entering target checkbox's state, and when they do
* it set's the parent's check state accordingly...
* Then it bubbles up to the parent's tier until it reaches a point
* of bubbling where the el is NOT an LI element.
*
* el should be a container for an input[type=checkbox] (eg li)
*
* @param {Element} el
*/
function checkRelatives(el) {
// Prevent bubbling up beyond the checkbox tree
if (el.prop("tagName") != "LI") return;
// should be an li element (hosting not only a checkbox, but also
// a ul of li's, each li containing checkboxes
var parent = getParentOfCurrentContainer(el);
var doTargetSiblingsOrParentSiblingsMatchTargetCheckState = checkIfSiblingsToEitherTargetOrParentsAreTheSameCheckStateAsTarget(el);
if (allSiblingsOfThisTierAreChecked()) {
markTheParentOfThisTierChecked(parent);
checkRelatives(parent);
}
else if (allCheckboxesOfThisTierAreUnchecked()) {
markTheParentOfThisTierUncheckedOrIndeterminate(parent);
checkRelatives(parent);
}
// The checkboxes of this tier are mix and matched, therefore
// we need to go up to every direct ancestor of this tier and set indeterminate
else {
markAllDirectAncestorsAsIndeterminate(el);
}
/**
* Returns true if all the siblings of the specified element
* (either target or one of it's direct anncestors) are of
* the same checkstate as the target checkbox originally
* clicked.
*
* @return {boolean}
*/
function allSiblingsOfThisTierAreChecked() {
return (doTargetSiblingsOrParentSiblingsMatchTargetCheckState && checkStateOfTarget);
}
/**
* Returns true if all the siblings of the specified element
* (either target or one of it's direct anncestors) are of a
* differing checkstate from the target originally clicked.
*
* @return {boolean}
*/
function allCheckboxesOfThisTierAreUnchecked() {
return (doTargetSiblingsOrParentSiblingsMatchTargetCheckState && !checkStateOfTarget);
}
/**
* Iterates over each of elContainer's siblings and if one
* contains a checkstate that differs from the originating
* checkbox's state, the function will return false.
* Else returns true.
*
* @param {Element} elContainer
* @return {boolean} !onesNotRight
*/
function checkIfSiblingsToEitherTargetOrParentsAreTheSameCheckStateAsTarget(elContainer) {
var onesNotRight = false;
elContainer.siblings().each(function() {
var doesSiblingStateMatchesTarget =
(getCheckboxOfContainer($(this)).prop("checked") === checkStateOfTarget);
if (!doesSiblingStateMatchesTarget)
onesNotRight = true;
});
// when onesNotRight is false, we should return true
return !onesNotRight;
}
}
/**
* Marks all the children of the elContainer containing the
* children of the originally clicked checkbox to share the
* the state of the originally clicked checkbox.
*
* @param {Element} elContainer
* @param {Element} checkStateOfTarget
*/
function markAnyChildrenToShareTheCheckStateOfTarget(elContainer, checkStateOfTarget) {
elContainer.find(selectorForCheckboxes).prop({
indeterminate: false,
checked: checkStateOfTarget
});
}
/**
* Set any direct ancestor checkboxes of elContainer to an
* indeterminate state.
*
* When Andre is un-ticked, the first occurance of this el is
* the li container to Andre's textbox/ label. The
* parents("li") are actually the containers to Giants and Tall
* Things
*
* @param {Element} elContainer
*/
function markAllDirectAncestorsAsIndeterminate(elContainer) {
getAllDirectAnccestorCheckboxes(elContainer).prop({
indeterminate: true,
checked: false
});
}
function markTheParentOfThisTierUncheckedOrIndeterminate(parent) {
getCheckboxOfContainer(parent).prop({
checked: false,
indeterminate: (parent.find(selectorForCheckboxes + ':checked').length > 0)
});
}
// parent should be an li, containing a checkbox.
function markTheParentOfThisTierChecked(parent) {
getCheckboxOfContainer(parent).prop({
indeterminate: false,
checked: true
});
}
/**
* This helper function is used for going from, say, a checkbox
* to it's container. Or from it's container, to the next up
* container.
*
* @param {Element} startingEl
* @param {int} steps
*/
function traverseDOMUpwards(startingEl, steps) {
var el = startingEl;
for (var i = 0; i < steps; i++)
el = el.parent();
return el;
}
function getParentOfCurrentContainer(container) {
return traverseDOMUpwards(container, stepsFromContainerToParentContainer);
}
function getListItemContainingCheckbox(checkboxEl) {
return traverseDOMUpwards(checkboxEl, stepsFromCheckboxToContainer);
}
/**
* Gets the input[type=checkbox] of the container element based
* on stepsFromCheckboxToContainer.
*
* @return {Element}
*/
function getCheckboxOfContainer(elContainer) {
var childCheckbox = elContainer;
var s = stepsFromCheckboxToContainer - 1;
for (var i = 0; i < s; i++) {
childCheckbox = childCheckbox.children();
}
childCheckbox = childCheckbox.children(selectorForCheckboxes);
return childCheckbox;
}
/**
* Starting at elContainer (eg li), this function will get all
* direct anncestor checkboxes.
*
*/
function getAllDirectAnccestorCheckboxes(elContainer) {
var childCheckbox = elContainer.parents("li");
var s = stepsFromCheckboxToContainer - 1;
for (var i = 0; i < s; i++) {
childCheckbox = childCheckbox.children();
}
childCheckbox = childCheckbox.children(selectorForCheckboxes);
return childCheckbox;
}
});
}
};
window.IndeterminateCheckbox = IndeterminateCheckbox;
})();