MDL-69525 core: Publish a single event when toggling slave checkboxes
[moodle.git] / lib / amd / src / checkbox-toggleall.js
CommitLineData
270fd3f5
AN
1// This file is part of Moodle - http://moodle.org/
2//
3// Moodle is free software: you can redistribute it and/or modify
4// it under the terms of the GNU General Public License as published by
5// the Free Software Foundation, either version 3 of the License, or
6// (at your option) any later version.
7//
8// Moodle is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
15
16/**
17 * A module to help with toggle select/deselect all.
18 *
19 * @module core/checkbox-toggleall
20 * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
21 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22 */
23define(['jquery', 'core/pubsub'], function($, PubSub) {
24
05b25140
JP
25 /**
26 * Whether event listeners have already been registered.
27 *
28 * @private
29 * @type {boolean}
30 */
270fd3f5
AN
31 var registered = false;
32
05b25140
JP
33 /**
34 * List of custom events that this module publishes.
35 *
36 * @private
37 * @type {{checkboxToggled: string}}
38 */
270fd3f5
AN
39 var events = {
40 checkboxToggled: 'core/checkbox-toggleall:checkboxToggled',
41 };
42
05b25140
JP
43 /**
44 * Fetches elements that are member of a given toggle group.
45 *
46 * @private
47 * @param {jQuery} root The root jQuery element.
48 * @param {string} toggleGroup The toggle group name that we're searching form.
49 * @param {boolean} exactMatch Whether we want an exact match we just want to match toggle groups that start with the given
50 * toggle group name.
51 * @returns {jQuery} The elements matching the given toggle group.
52 */
0a3d3063
JP
53 var getToggleGroupElements = function(root, toggleGroup, exactMatch) {
54 if (exactMatch) {
55 return root.find('[data-action="toggle"][data-togglegroup="' + toggleGroup + '"]');
56 } else {
05b25140 57 return root.find('[data-action="toggle"][data-togglegroup^="' + toggleGroup + '"]');
0a3d3063 58 }
270fd3f5
AN
59 };
60
05b25140
JP
61 /**
62 * Fetches the slave checkboxes for a given toggle group.
63 *
64 * @private
65 * @param {jQuery} root The root jQuery element.
66 * @param {string} toggleGroup The toggle group name.
67 * @returns {jQuery} The slave checkboxes belonging to the toggle group.
68 */
270fd3f5 69 var getAllSlaveCheckboxes = function(root, toggleGroup) {
05b25140 70 return getToggleGroupElements(root, toggleGroup, false).filter('[data-toggle="slave"]');
270fd3f5
AN
71 };
72
05b25140
JP
73 /**
74 * Fetches the master elements (checkboxes or buttons) that control the slave checkboxes in a given toggle group.
75 *
76 * @private
77 * @param {jQuery} root The root jQuery element.
78 * @param {string} toggleGroup The toggle group name.
79 * @param {boolean} exactMatch
80 * @returns {jQuery} The control elements belonging to the toggle group.
81 */
0a3d3063
JP
82 var getControlCheckboxes = function(root, toggleGroup, exactMatch) {
83 return getToggleGroupElements(root, toggleGroup, exactMatch).filter('[data-toggle="master"]');
686ab8cc
JP
84 };
85
05b25140
JP
86 /**
87 * Fetches the action elements that perform actions on the selected checkboxes in a given toggle group.
88 *
89 * @private
90 * @param {jQuery} root The root jQuery element.
91 * @param {string} toggleGroup The toggle group name.
92 * @returns {jQuery} The action elements belonging to the toggle group.
93 */
686ab8cc 94 var getActionElements = function(root, toggleGroup) {
0a3d3063 95 return getToggleGroupElements(root, toggleGroup, true).filter('[data-toggle="action"]');
270fd3f5
AN
96 };
97
05b25140
JP
98 /**
99 * Toggles the slave checkboxes in a given toggle group when a master element in that toggle group is toggled.
100 *
101 * @private
102 * @param {Object} e The event object.
103 */
270fd3f5
AN
104 var toggleSlavesFromMasters = function(e) {
105 var root = e.data.root;
106 var target = $(e.target);
107
108 var toggleGroupName = target.data('togglegroup');
5943fc3f
JP
109 var targetState;
110 if (target.is(':checkbox')) {
111 targetState = target.is(':checked');
112 } else {
113 targetState = target.data('checkall') === 1;
114 }
270fd3f5 115
814da167
AN
116 toggleSlavesToState(root, toggleGroupName, targetState);
117 };
118
119 /**
120 * Toggles the slave checkboxes from the masters.
121 *
122 * @param {HTMLElement} root
123 * @param {String} toggleGroupName
124 */
125 var updateSlavesFromMasterState = function(root, toggleGroupName) {
126 // Normalise to jQuery Object.
127 root = $(root);
128
129 var target = getControlCheckboxes(root, toggleGroupName, false);
130 var targetState;
131 if (target.is(':checkbox')) {
132 targetState = target.is(':checked');
133 } else {
134 targetState = target.data('checkall') === 1;
135 }
136
137 toggleSlavesToState(root, toggleGroupName, targetState);
138 };
139
775dea63
MG
140 /**
141 * Toggles the master checkboxes and action elements in a given toggle group.
142 *
143 * @param {jQuery} root The root jQuery element.
144 * @param {String} toggleGroupName The name of the toggle group
145 */
146 var toggleMastersAndActionElements = function(root, toggleGroupName) {
147 var toggleGroupSlaves = getAllSlaveCheckboxes(root, toggleGroupName);
148 if (toggleGroupSlaves.length > 0) {
149 var toggleGroupCheckedSlaves = toggleGroupSlaves.filter(':checked');
150 var targetState = toggleGroupSlaves.length === toggleGroupCheckedSlaves.length;
151
152 // Make sure to toggle the exact master checkbox in the given toggle group.
153 setMasterStates(root, toggleGroupName, targetState, true);
154 // Enable the action elements if there's at least one checkbox checked in the given toggle group.
155 // Disable otherwise.
156 setActionElementStates(root, toggleGroupName, !toggleGroupCheckedSlaves.length);
157 }
158 };
159
160 /**
161 * Returns an array containing every toggle group level of a given toggle group.
162 *
163 * @param {String} toggleGroupName The name of the toggle group
164 * @return {Array} toggleGroupLevels Array that contains every toggle group level of a given toggle group
165 */
166 var getToggleGroupLevels = function(toggleGroupName) {
167 var toggleGroups = toggleGroupName.split(' ');
168 var toggleGroupLevels = [];
169 var toggleGroupLevel = '';
170
171 toggleGroups.forEach(function(toggleGroupName) {
172 toggleGroupLevel += ' ' + toggleGroupName;
173 toggleGroupLevels.push(toggleGroupLevel.trim());
174 });
175
176 return toggleGroupLevels;
177 };
178
814da167
AN
179 /**
180 * Toggles the slave checkboxes to a specific state.
181 *
182 * @param {HTMLElement} root
183 * @param {String} toggleGroupName
184 * @param {Bool} targetState
185 */
186 var toggleSlavesToState = function(root, toggleGroupName, targetState) {
270fd3f5 187 var slaves = getAllSlaveCheckboxes(root, toggleGroupName);
775dea63
MG
188 // Set the slave checkboxes from the masters and manually trigger the native 'change' event.
189 slaves.prop('checked', targetState).trigger('change');
190 // Get all checked slaves after the change of state.
270fd3f5
AN
191 var checkedSlaves = slaves.filter(':checked');
192
775dea63 193 // Toggle the master checkbox in the given toggle group.
05b25140 194 setMasterStates(root, toggleGroupName, targetState, false);
775dea63
MG
195 // Enable the action elements if there's at least one checkbox checked in the given toggle group. Disable otherwise.
196 setActionElementStates(root, toggleGroupName, !checkedSlaves.length);
197
198 // Get all toggle group levels and toggle accordingly all parent master checkboxes and action elements from each
199 // level. Exclude the given toggle group (toggleGroupName) as the master checkboxes and action elements from this
200 // level have been already toggled.
201 var toggleGroupLevels = getToggleGroupLevels(toggleGroupName)
202 .filter(toggleGroupLevel => toggleGroupLevel !== toggleGroupName);
203
204 toggleGroupLevels.forEach(function(toggleGroupLevel) {
205 // Toggle the master checkboxes action elements in the given toggle group level.
206 toggleMastersAndActionElements(root, toggleGroupLevel);
207 });
686ab8cc 208
270fd3f5
AN
209 PubSub.publish(events.checkboxToggled, {
210 root: root,
211 toggleGroupName: toggleGroupName,
212 slaves: slaves,
213 checkedSlaves: checkedSlaves,
214 anyChecked: targetState,
215 });
216 };
217
814da167
AN
218 /**
219 * Set the state for an entire group of checkboxes.
220 *
221 * @param {HTMLElement} root
222 * @param {String} toggleGroupName
223 * @param {Bool} targetState
224 */
225 var setGroupState = function(root, toggleGroupName, targetState) {
226 // Normalise to jQuery Object.
227 root = $(root);
228
229 // Set the master and slaves.
230 setMasterStates(root, toggleGroupName, targetState, true);
231 toggleSlavesToState(root, toggleGroupName, targetState);
232 };
233
05b25140
JP
234 /**
235 * Toggles the master checkboxes in a given toggle group when all or none of the slave checkboxes in the same toggle group
236 * have been selected.
237 *
238 * @private
239 * @param {Object} e The event object.
240 */
270fd3f5
AN
241 var toggleMastersFromSlaves = function(e) {
242 var root = e.data.root;
243 var target = $(e.target);
775dea63
MG
244 var toggleGroupName = target.data('togglegroup');
245 var slaves = getAllSlaveCheckboxes(root, toggleGroupName);
246 var checkedSlaves = slaves.filter(':checked');
270fd3f5 247
775dea63
MG
248 // Get all toggle group levels for the given toggle group and toggle accordingly all master checkboxes
249 // and action elements from each level.
250 var toggleGroupLevels = getToggleGroupLevels(toggleGroupName);
251 toggleGroupLevels.forEach(function(toggleGroupLevel) {
252 // Toggle the master checkboxes action elements in the given toggle group level.
253 toggleMastersAndActionElements(root, toggleGroupLevel);
0a3d3063 254 });
270fd3f5 255
775dea63
MG
256 PubSub.publish(events.checkboxToggled, {
257 root: root,
258 toggleGroupName: toggleGroupName,
259 slaves: slaves,
260 checkedSlaves: checkedSlaves,
261 anyChecked: !!checkedSlaves.length,
270fd3f5
AN
262 });
263 };
264
05b25140
JP
265 /**
266 * Enables or disables the action elements.
267 *
268 * @private
269 * @param {jQuery} root The root jQuery element.
270 * @param {string} toggleGroupName The toggle group name of the action element(s).
271 * @param {boolean} disableActionElements Whether to disable or to enable the action elements.
272 */
686ab8cc
JP
273 var setActionElementStates = function(root, toggleGroupName, disableActionElements) {
274 getActionElements(root, toggleGroupName).prop('disabled', disableActionElements);
275 };
276
05b25140
JP
277 /**
278 * Selects or deselects the master elements.
279 *
280 * @private
281 * @param {jQuery} root The root jQuery element.
282 * @param {string} toggleGroupName The toggle group name of the master element(s).
283 * @param {boolean} targetState Whether to select (true) or deselect (false).
284 * @param {boolean} exactMatch Whether to do an exact match for the toggle group name or not.
285 */
0a3d3063 286 var setMasterStates = function(root, toggleGroupName, targetState, exactMatch) {
270fd3f5 287 // Set the master checkboxes value and ARIA labels..
0a3d3063 288 var masters = getControlCheckboxes(root, toggleGroupName, exactMatch);
270fd3f5 289 masters.prop('checked', targetState);
5943fc3f
JP
290 masters.each(function(i, masterElement) {
291 masterElement = $(masterElement);
292
270fd3f5 293 var targetString;
5943fc3f
JP
294 if (targetState) {
295 targetString = masterElement.data('toggle-deselectall');
296 } else {
297 targetString = masterElement.data('toggle-selectall');
298 }
270fd3f5 299
5943fc3f
JP
300 if (masterElement.is(':checkbox')) {
301 var masterLabel = root.find('[for="' + masterElement.attr('id') + '"]');
302 if (masterLabel.length) {
303 if (masterLabel.html() !== targetString) {
304 masterLabel.html(targetString);
305 }
270fd3f5 306 }
5943fc3f
JP
307 } else {
308 masterElement.text(targetString);
309 // Set the checkall data attribute.
310 masterElement.data('checkall', targetState ? 0 : 1);
270fd3f5
AN
311 }
312 });
313 };
314
05b25140
JP
315 /**
316 * Registers the event listeners.
317 *
318 * @private
319 */
270fd3f5
AN
320 var registerListeners = function() {
321 if (!registered) {
322 registered = true;
323
324 var root = $(document.body);
5943fc3f 325 root.on('click', '[data-action="toggle"][data-toggle="master"]', {root: root}, toggleSlavesFromMasters);
775dea63 326 root.on('click', '[data-action="toggle"][data-toggle="slave"]', {root: root}, toggleMastersFromSlaves);
270fd3f5
AN
327 }
328 };
329
330 return {
331 init: function() {
332 registerListeners();
333 },
334 events: events,
814da167
AN
335 setGroupState: setGroupState,
336 updateSlavesFromMasterState: updateSlavesFromMasterState,
270fd3f5
AN
337 };
338});