473a514ad9c2117644da81fe38dc97203e3c07af
[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 slave checkboxes to a specific state.
142      *
143      * @param {HTMLElement} root
144      * @param {String} toggleGroupName
145      * @param {Bool} targetState
146      */
147     var toggleSlavesToState = function(root, toggleGroupName, targetState) {
148         var slaves = getAllSlaveCheckboxes(root, toggleGroupName);
149         var checkedSlaves = slaves.filter(':checked');
151         setMasterStates(root, toggleGroupName, targetState, false);
153         // Set the slave checkboxes from the masters.
154         slaves.prop('checked', targetState);
155         // Trigger 'change' event to toggle other master checkboxes (e.g. parent master checkboxes) and action elements.
156         slaves.trigger('change');
158         PubSub.publish(events.checkboxToggled, {
159             root: root,
160             toggleGroupName: toggleGroupName,
161             slaves: slaves,
162             checkedSlaves: checkedSlaves,
163             anyChecked: targetState,
164         });
165     };
167     /**
168      * Set the state for an entire group of checkboxes.
169      *
170      * @param {HTMLElement} root
171      * @param {String} toggleGroupName
172      * @param {Bool} targetState
173      */
174     var setGroupState = function(root, toggleGroupName, targetState) {
175         // Normalise to jQuery Object.
176         root = $(root);
178         // Set the master and slaves.
179         setMasterStates(root, toggleGroupName, targetState, true);
180         toggleSlavesToState(root, toggleGroupName, targetState);
181     };
183     /**
184      * Toggles the master checkboxes in a given toggle group when all or none of the slave checkboxes in the same toggle group
185      * have been selected.
186      *
187      * @private
188      * @param {Object} e The event object.
189      */
190     var toggleMastersFromSlaves = function(e) {
191         var root = e.data.root;
192         var target = $(e.target);
194         var toggleGroups = target.data('togglegroup').split(' ');
195         var toggleGroupLevels = [];
196         var toggleGroupLevel = '';
197         toggleGroups.forEach(function(toggleGroupName) {
198             toggleGroupLevel += ' ' + toggleGroupName;
199             toggleGroupLevels.push(toggleGroupLevel.trim());
200         });
202         toggleGroupLevels.forEach(function(toggleGroupName) {
203             var slaves = getAllSlaveCheckboxes(root, toggleGroupName);
204             var checkedSlaves = slaves.filter(':checked');
205             var targetState = (slaves.length === checkedSlaves.length);
207             // Make sure to toggle the exact master checkbox.
208             setMasterStates(root, toggleGroupName, targetState, true);
210             // Enable action elements when there's at least one checkbox checked. Disable otherwise.
211             setActionElementStates(root, toggleGroupName, !checkedSlaves.length);
213             PubSub.publish(events.checkboxToggled, {
214                 root: root,
215                 toggleGroupName: toggleGroupName,
216                 slaves: slaves,
217                 checkedSlaves: checkedSlaves,
218                 anyChecked: !!checkedSlaves.length,
219             });
220         });
221     };
223     /**
224      * Enables or disables the action elements.
225      *
226      * @private
227      * @param {jQuery} root The root jQuery element.
228      * @param {string} toggleGroupName The toggle group name of the action element(s).
229      * @param {boolean} disableActionElements Whether to disable or to enable the action elements.
230      */
231     var setActionElementStates = function(root, toggleGroupName, disableActionElements) {
232         getActionElements(root, toggleGroupName).prop('disabled', disableActionElements);
233     };
235     /**
236      * Selects or deselects the master elements.
237      *
238      * @private
239      * @param {jQuery} root The root jQuery element.
240      * @param {string} toggleGroupName The toggle group name of the master element(s).
241      * @param {boolean} targetState Whether to select (true) or deselect (false).
242      * @param {boolean} exactMatch Whether to do an exact match for the toggle group name or not.
243      */
244     var setMasterStates = function(root, toggleGroupName, targetState, exactMatch) {
245         // Set the master checkboxes value and ARIA labels..
246         var masters = getControlCheckboxes(root, toggleGroupName, exactMatch);
247         masters.prop('checked', targetState);
248         masters.each(function(i, masterElement) {
249             masterElement = $(masterElement);
251             var targetString;
252             if (targetState) {
253                 targetString = masterElement.data('toggle-deselectall');
254             } else {
255                 targetString = masterElement.data('toggle-selectall');
256             }
258             if (masterElement.is(':checkbox')) {
259                 var masterLabel = root.find('[for="' + masterElement.attr('id') + '"]');
260                 if (masterLabel.length) {
261                     if (masterLabel.html() !== targetString) {
262                         masterLabel.html(targetString);
263                     }
264                 }
265             } else {
266                 masterElement.text(targetString);
267                 // Set the checkall data attribute.
268                 masterElement.data('checkall', targetState ? 0 : 1);
269             }
270         });
271     };
273     /**
274      * Registers the event listeners.
275      *
276      * @private
277      */
278     var registerListeners = function() {
279         if (!registered) {
280             registered = true;
282             var root = $(document.body);
283             root.on('click', '[data-action="toggle"][data-toggle="master"]', {root: root}, toggleSlavesFromMasters);
284             root.on('change', '[data-action="toggle"][data-toggle="slave"]', {root: root}, toggleMastersFromSlaves);
285         }
286     };
288     return {
289         init: function() {
290             registerListeners();
291         },
292         events: events,
293         setGroupState: setGroupState,
294         updateSlavesFromMasterState: updateSlavesFromMasterState,
295     };
296 });