Merge branch 'MDL-57896-master-clicfg' of git://github.com/mudrd8mz/moodle
[moodle.git] / user / selector / module.js
blob696e8d88a0d7740969d270d4c8a7d815c2973b94
1 /**
2  * JavaScript for the user selectors.
3  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4  * @package userselector
5  */
7 // Define the core_user namespace if it has not already been defined
8 M.core_user = M.core_user || {};
9 // Define a user selectors array for against the cure_user namespace
10 M.core_user.user_selectors = [];
11 /**
12  * Retrieves an instantiated user selector or null if there isn't one by the requested name
13  * @param {string} name The name of the selector to retrieve
14  * @return bool
15  */
16 M.core_user.get_user_selector = function (name) {
17     return this.user_selectors[name] || null;
20 /**
21  * Initialise a new user selector.
22  *
23  * @param {YUI} Y The YUI3 instance
24  * @param {string} name the control name/id.
25  * @param {string} hash the hash that identifies this selector in the user's session.
26  * @param {array} extrafields extra fields we are displaying for each user in addition to fullname.
27  * @param {string} lastsearch The last search that took place
28  */
29 M.core_user.init_user_selector = function (Y, name, hash, extrafields, lastsearch) {
30     // Creates a new user_selector object
31     var user_selector = {
32         /** This id/name used for this control in the HTML. */
33         name : name,
34         /** Array of fields to display for each user, in addition to fullname. */
35         extrafields: extrafields,
36         /** Number of seconds to delay before submitting a query request */
37         querydelay : 0.5,
38         /** The input element that contains the search term. */
39         searchfield : Y.one('#' + name + '_searchtext'),
40         /** The clear button. */
41         clearbutton : null,
42         /** The select element that contains the list of users. */
43         listbox : Y.one('#' + name),
44         /** Used to hold the timeout id of the timeout that waits before doing a search. */
45         timeoutid : null,
46         /** Stores any in-progress remote requests. */
47         iotransactions : {},
48         /** The last string that we searched for, so we can avoid unnecessary repeat searches. */
49         lastsearch : lastsearch,
50         /** Whether any options where selected last time we checked. Used by
51          *  handle_selection_change to track when this status changes. */
52         selectionempty : true,
53         /**
54          * Initialises the user selector object
55          * @constructor
56          */
57         init : function() {
58             // Hide the search button and replace it with a label.
59             var searchbutton = Y.one('#' + this.name + '_searchbutton');
60             this.searchfield.insert(Y.Node.create('<label for="' + this.name + '_searchtext">' + searchbutton.get('value') + '</label>'), this.searchfield);
61             searchbutton.remove();
63             // Hook up the event handler for when the search text changes.
64             this.searchfield.on('keyup', this.handle_keyup, this);
66             // Hook up the event handler for when the selection changes.
67             this.listbox.on('keyup', this.handle_selection_change, this);
68             this.listbox.on('click', this.handle_selection_change, this);
69             this.listbox.on('change', this.handle_selection_change, this);
71             // And when the search any substring preference changes. Do an immediate re-search.
72             Y.one('#userselector_searchanywhereid').on('click', this.handle_searchanywhere_change, this);
74             // Define our custom event.
75             //this.createEvent('selectionchanged');
76             this.selectionempty = this.is_selection_empty();
78             // Replace the Clear submit button with a clone that is not a submit button.
79             var clearbtn = Y.one('#' + this.name + '_clearbutton');
80             this.clearbutton = Y.Node.create('<input type="button" value="' + clearbtn.get('value') + '" class="btn btn-secondary m-x-1"/>');
81             clearbtn.replace(Y.Node.getDOMNode(this.clearbutton));
82             this.clearbutton.set('id', this.name + "_clearbutton");
83             this.clearbutton.on('click', this.handle_clear, this);
84             this.clearbutton.set('disabled', (this.get_search_text() == ''));
86             this.send_query(false);
87         },
88         /**
89          * Key up hander for the search text box.
90          * @param {Y.Event} e the keyup event.
91          */
92         handle_keyup : function(e) {
93             //  Trigger an ajax search after a delay.
94             this.cancel_timeout();
95             this.timeoutid = Y.later(this.querydelay * 1000, e, function(obj){obj.send_query(false)}, this);
97             // Enable or diable the clear button.
98             this.clearbutton.set('disabled', (this.get_search_text() == ''));
100             // If enter was pressed, prevent a form submission from happening.
101             if (e.keyCode == 13) {
102                 e.halt();
103             }
104         },
105         /**
106          * Handles when the selection has changed. If the selection has changed from
107          * empty to not-empty, or vice versa, then fire the event handlers.
108          */
109         handle_selection_change : function() {
110             var isselectionempty = this.is_selection_empty();
111             if (isselectionempty !== this.selectionempty) {
112                 this.fire('user_selector:selectionchanged', isselectionempty);
113             }
114             this.selectionempty = isselectionempty;
115         },
116         /**
117          * Trigger a re-search when the 'search any substring' option is changed.
118          */
119         handle_searchanywhere_change : function() {
120             if (this.lastsearch != '' && this.get_search_text() != '') {
121                 this.send_query(true);
122             }
123         },
124         /**
125          * Click handler for the clear button..
126          */
127         handle_clear : function() {
128             this.searchfield.set('value', '');
129             this.clearbutton.set('disabled',true);
130             this.send_query(false);
131         },
132         /**
133          * Fires off the ajax search request.
134          */
135         send_query : function(forceresearch) {
136             // Cancel any pending timeout.
137             this.cancel_timeout();
139             var value = this.get_search_text();
140             this.searchfield.set('class', '');
141             if (this.lastsearch == value && !forceresearch) {
142                 return;
143             }
145             // Try to cancel existing transactions.
146             Y.Object.each(this.iotransactions, function(trans) {
147                 trans.abort();
148             });
150             var iotrans = Y.io(M.cfg.wwwroot + '/user/selector/search.php', {
151                 method: 'POST',
152                 data: 'selectorid=' + hash + '&sesskey=' + M.cfg.sesskey + '&search=' + value + '&userselector_searchanywhere=' + this.get_option('searchanywhere'),
153                 on: {
154                     complete: this.handle_response
155                 },
156                 context:this
157             });
158             this.iotransactions[iotrans.id] = iotrans;
160             this.lastsearch = value;
161             this.listbox.setStyle('background','url(' + M.util.image_url('i/loading', 'moodle') + ') no-repeat center center');
162         },
163         /**
164          * Handle what happens when we get some data back from the search.
165          * @param {int} requestid not used.
166          * @param {object} response the list of users that was returned.
167          */
168         handle_response : function(requestid, response) {
169             try {
170                 delete this.iotransactions[requestid];
171                 if (!Y.Object.isEmpty(this.iotransactions)) {
172                     // More searches pending. Wait until they are all done.
173                     return;
174                 }
175                 this.listbox.setStyle('background','');
176                 var data = Y.JSON.parse(response.responseText);
177                 if (data.error) {
178                     this.searchfield.addClass('error');
179                     return new M.core.ajaxException(data);
180                 }
181                 this.output_options(data);
183                 // If updated userSummaries are present, overwrite the global variable
184                 // that's output by group_non_members_selector::print_user_summaries() in user/selector/lib.php
185                 if (typeof data.userSummaries !== "undefined") {
186                     /* global userSummaries:true */
187                     /* exported userSummaries */
188                     userSummaries = data.userSummaries;
189                 }
190             } catch (e) {
191                 this.listbox.setStyle('background','');
192                 this.searchfield.addClass('error');
193                 return new M.core.exception(e);
194             }
195         },
196         /**
197          * This method should do the same sort of thing as the PHP method
198          * user_selector_base::output_options.
199          * @param {object} data the list of users to populate the list box with.
200          */
201         output_options : function(data) {
202             // Clear out the existing options, keeping any ones that are already selected.
203             var selectedusers = {};
204             this.listbox.all('optgroup').each(function(optgroup){
205                 optgroup.all('option').each(function(option){
206                     if (option.get('selected')) {
207                         selectedusers[option.get('value')] = {
208                             id : option.get('value'),
209                             name : option.get('innerText') || option.get('textContent'),
210                             disabled: option.get('disabled')
211                         }
212                     }
213                     option.remove();
214                 }, this);
215                 optgroup.remove();
216             }, this);
218             // Output each optgroup.
219             var count = 0;
220             for (var key in data.results) {
221                 var groupdata = data.results[key];
222                 this.output_group(groupdata.name, groupdata.users, selectedusers, true);
223                 count ++;
224             }
225             if (!count) {
226                 var searchstr = (this.lastsearch != '') ? this.insert_search_into_str(M.util.get_string('nomatchingusers', 'moodle'), this.lastsearch) : M.util.get_string('none', 'moodle');
227                 this.output_group(searchstr, {}, selectedusers, true)
228             }
230             // If there were previously selected users who do not match the search, show them too.
231             if (this.get_option('preserveselected') && selectedusers) {
232                 this.output_group(this.insert_search_into_str(M.util.get_string('previouslyselectedusers', 'moodle'), this.lastsearch), selectedusers, true, false);
233             }
234             this.handle_selection_change();
235         },
236         /**
237          * This method should do the same sort of thing as the PHP method
238          * user_selector_base::output_optgroup.
239          *
240          * @param {string} groupname the label for this optgroup.v
241          * @param {object} users the users to put in this optgroup.
242          * @param {boolean|object} selectedusers if true, select the users in this group.
243          * @param {boolean} processsingle
244          */
245         output_group : function(groupname, users, selectedusers, processsingle) {
246             var optgroup = Y.Node.create('<optgroup></optgroup>');
247             var count = 0;
248             for (var key in users) {
249                 var user = users[key];
250                 var option = Y.Node.create('<option value="' + user.id + '">' + user.name + '</option>');
251                 if (user.disabled) {
252                     option.set('disabled', true);
253                 } else if (selectedusers === true || selectedusers[user.id]) {
254                     option.set('selected', true);
255                     delete selectedusers[user.id];
256                 } else {
257                     option.set('selected', false);
258                 }
259                 optgroup.append(option);
260                 if (user.infobelow) {
261                     extraoption = Y.Node.create('<option disabled="disabled" class="userselector-infobelow"/>');
262                     extraoption.appendChild(document.createTextNode(user.infobelow));
263                     optgroup.append(extraoption);
264                 }
265                 count ++;
266             }
268             if (count > 0) {
269                 optgroup.set('label', groupname + ' (' + count + ')');
270                 if (processsingle && count === 1 && this.get_option('autoselectunique') && option.get('disabled') == false) {
271                     option.set('selected', true);
272                 }
273             } else {
274                 optgroup.set('label', groupname);
275                 optgroup.append(Y.Node.create('<option disabled="disabled">\u00A0</option>'));
276             }
277             this.listbox.append(optgroup);
278         },
279         /**
280          * Replace
281          * @param {string} str
282          * @param {string} search The search term
283          * @return string
284          */
285         insert_search_into_str : function(str, search) {
286             return str.replace("%%SEARCHTERM%%", search);
287         },
288         /**
289          * Gets the search text
290          * @return String the value to search for, with leading and trailing whitespace trimmed.
291          */
292         get_search_text : function() {
293             return this.searchfield.get('value').toString().replace(/^ +| +$/, '');
294         },
295         /**
296          * Returns true if the selection is empty (nothing is selected)
297          * @return Boolean check all the options and return whether any are selected.
298          */
299         is_selection_empty : function() {
300             var selection = false;
301             this.listbox.all('option').each(function(){
302                 if (this.get('selected')) {
303                     selection = true;
304                 }
305             });
306             return !(selection);
307         },
308         /**
309          * Cancel the search delay timeout, if there is one.
310          */
311         cancel_timeout : function() {
312             if (this.timeoutid) {
313                 clearTimeout(this.timeoutid);
314                 this.timeoutid = null;
315             }
316         },
317         /**
318          * @param {string} name The name of the option to retrieve
319          * @return the value of one of the option checkboxes.
320          */
321         get_option : function(name) {
322             var checkbox = Y.one('#userselector_' + name + 'id');
323             if (checkbox) {
324                 return (checkbox.get('checked'));
325             } else {
326                 return false;
327             }
328         }
329     };
330     // Augment the user selector with the EventTarget class so that we can use
331     // custom events
332     Y.augment(user_selector, Y.EventTarget, null, null, {});
333     // Initialise the user selector
334     user_selector.init();
335     // Store the user selector so that it can be retrieved
336     this.user_selectors[name] = user_selector;
337     // Return the user selector
338     return user_selector;
342  * Initialise a class that updates the user's preferences when they change one of
343  * the options checkboxes.
344  * @constructor
345  * @param {YUI} Y
346  * @return Tracker object
347  */
348 M.core_user.init_user_selector_options_tracker = function(Y) {
349     // Create a user selector options tracker
350     var user_selector_options_tracker = {
351         /**
352          * Initlises the option tracker and gets everything going.
353          * @constructor
354          */
355         init : function() {
356             var settings = [
357                 'userselector_preserveselected',
358                 'userselector_autoselectunique',
359                 'userselector_searchanywhere'
360             ];
361             for (var s in settings) {
362                 var setting = settings[s];
363                 Y.one('#' + setting + 'id').on('click', this.set_user_preference, this, setting);
364             }
365         },
366         /**
367          * Sets a user preference for the options tracker
368          * @param {Y.Event|null} e
369          * @param {string} name The name of the preference to set
370          */
371         set_user_preference : function(e, name) {
372             M.util.set_user_preference(name, Y.one('#' + name + 'id').get('checked'));
373         }
374     };
375     // Initialise the options tracker
376     user_selector_options_tracker.init();
377     // Return it just incase it is ever wanted
378     return user_selector_options_tracker;