MDL-69525 core: Publish a single event when toggling slave checkboxes
[moodle.git] / lib / amd / src / checkbox-toggleall.js
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/>.
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  */
23 define(['jquery', 'core/pubsub'], function($, PubSub) {
25     /**
26      * Whether event listeners have already been registered.
27      *
28      * @private
29      * @type {boolean}
30      */
31     var registered = false;
33     /**
34      * List of custom events that this module publishes.
35      *
36      * @private
37      * @type {{checkboxToggled: string}}
38      */
39     var events = {
40         checkboxToggled: 'core/checkbox-toggleall:checkboxToggled',
41     };
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      */
53     var getToggleGroupElements = function(root, toggleGroup, exactMatch) {
54         if (exactMatch) {
55             return root.find('[data-action="toggle"][data-togglegroup="' + toggleGroup + '"]');
56         } else {
57             return root.find('[data-action="toggle"][data-togglegroup^="' + toggleGroup + '"]');
58         }
59     };
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      */
69     var getAllSlaveCheckboxes = function(root, toggleGroup) {
70         return getToggleGroupElements(root, toggleGroup, false).filter('[data-toggle="slave"]');
71     };
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      */
82     var getControlCheckboxes = function(root, toggleGroup, exactMatch) {
83         return getToggleGroupElements(root, toggleGroup, exactMatch).filter('[data-toggle="master"]');
84     };
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      */
94     var getActionElements = function(root, toggleGroup) {
95         return getToggleGroupElements(root, toggleGroup, true).filter('[data-toggle="action"]');
96     };
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      */
104     var toggleSlavesFromMasters = function(e) {
105         var root = e.data.root;
106         var target = $(e.target);
108         var toggleGroupName = target.data('togglegroup');
109         var targetState;
110         if (target.is(':checkbox')) {
111             targetState = target.is(':checked');
112         } else {
113             targetState = target.data('checkall') === 1;
114         }
116         toggleSlavesToState(root, toggleGroupName, targetState);
117     };
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);
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         }
137         toggleSlavesToState(root, toggleGroupName, targetState);
138     };
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;
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     };
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 = '';
171         toggleGroups.forEach(function(toggleGroupName) {
172             toggleGroupLevel += ' ' + toggleGroupName;
173             toggleGroupLevels.push(toggleGroupLevel.trim());
174         });
176         return toggleGroupLevels;
177     };
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) {
187         var slaves = getAllSlaveCheckboxes(root, toggleGroupName);
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.
191         var checkedSlaves = slaves.filter(':checked');
193         // Toggle the master checkbox in the given toggle group.
194         setMasterStates(root, toggleGroupName, targetState, false);
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);
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);
204         toggleGroupLevels.forEach(function(toggleGroupLevel) {
205             // Toggle the master checkboxes action elements in the given toggle group level.
206             toggleMastersAndActionElements(root, toggleGroupLevel);
207         });
209         PubSub.publish(events.checkboxToggled, {
210             root: root,
211             toggleGroupName: toggleGroupName,
212             slaves: slaves,
213             checkedSlaves: checkedSlaves,
214             anyChecked: targetState,
215         });
216     };
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);
229         // Set the master and slaves.
230         setMasterStates(root, toggleGroupName, targetState, true);
231         toggleSlavesToState(root, toggleGroupName, targetState);
232     };
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      */
241     var toggleMastersFromSlaves = function(e) {
242         var root = e.data.root;
243         var target = $(e.target);
244         var toggleGroupName = target.data('togglegroup');
245         var slaves = getAllSlaveCheckboxes(root, toggleGroupName);
246         var checkedSlaves = slaves.filter(':checked');
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);
254         });
256         PubSub.publish(events.checkboxToggled, {
257             root: root,
258             toggleGroupName: toggleGroupName,
259             slaves: slaves,
260             checkedSlaves: checkedSlaves,
261             anyChecked: !!checkedSlaves.length,
262         });
263     };
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      */
273     var setActionElementStates = function(root, toggleGroupName, disableActionElements) {
274         getActionElements(root, toggleGroupName).prop('disabled', disableActionElements);
275     };
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      */
286     var setMasterStates = function(root, toggleGroupName, targetState, exactMatch) {
287         // Set the master checkboxes value and ARIA labels..
288         var masters = getControlCheckboxes(root, toggleGroupName, exactMatch);
289         masters.prop('checked', targetState);
290         masters.each(function(i, masterElement) {
291             masterElement = $(masterElement);
293             var targetString;
294             if (targetState) {
295                 targetString = masterElement.data('toggle-deselectall');
296             } else {
297                 targetString = masterElement.data('toggle-selectall');
298             }
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                     }
306                 }
307             } else {
308                 masterElement.text(targetString);
309                 // Set the checkall data attribute.
310                 masterElement.data('checkall', targetState ? 0 : 1);
311             }
312         });
313     };
315     /**
316      * Registers the event listeners.
317      *
318      * @private
319      */
320     var registerListeners = function() {
321         if (!registered) {
322             registered = true;
324             var root = $(document.body);
325             root.on('click', '[data-action="toggle"][data-toggle="master"]', {root: root}, toggleSlavesFromMasters);
326             root.on('click', '[data-action="toggle"][data-toggle="slave"]', {root: root}, toggleMastersFromSlaves);
327         }
328     };
330     return {
331         init: function() {
332             registerListeners();
333         },
334         events: events,
335         setGroupState: setGroupState,
336         updateSlavesFromMasterState: updateSlavesFromMasterState,
337     };
338 });