Merge branch 'wip-mdl-34606-m23' of git://github.com/rajeshtaneja/moodle into MOODLE_...
[moodle.git] / user / selector / module.js
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;
18 };
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         /** The last string that we searched for, so we can avoid unnecessary repeat searches. */
47         lastsearch : lastsearch,
48         /** Whether any options where selected last time we checked. Used by
49          *  handle_selection_change to track when this status changes. */
50         selectionempty : true,
51         /**
52          * Initialises the user selector object
53          * @constructor
54          */
55         init : function() {
56             // Hide the search button and replace it with a label.
57             var searchbutton = Y.one('#'+this.name + '_searchbutton');
58             this.searchfield.insert(Y.Node.create('<label for="'+this.name + '_searchtext">'+searchbutton.get('value')+'</label>'), this.searchfield);
59             searchbutton.remove();
61             // Hook up the event handler for when the search text changes.
62             this.searchfield.on('keyup', this.handle_keyup, this);
64             // Hook up the event handler for when the selection changes.
65             this.listbox.on('keyup', this.handle_selection_change, this);
66             this.listbox.on('click', this.handle_selection_change, this);
67             this.listbox.on('change', this.handle_selection_change, this);
69             // And when the search any substring preference changes. Do an immediate re-search.
70             Y.one('#userselector_searchanywhereid').on('click', this.handle_searchanywhere_change, this);
72             // Define our custom event.
73             //this.createEvent('selectionchanged');
74             this.selectionempty = this.is_selection_empty();
76             // Replace the Clear submit button with a clone that is not a submit button.
77             var clearbtn = Y.one('#'+this.name + '_clearbutton');
78             this.clearbutton = Y.Node.create('<input type="button" value="'+clearbtn.get('value')+'" />');
79             clearbtn.replace(Y.Node.getDOMNode(this.clearbutton));
80             this.clearbutton.set('id', this.name+"_clearbutton");
81             this.clearbutton.on('click', this.handle_clear, this);
82             this.clearbutton.set('disabled', (this.get_search_text() == ''));
84             this.send_query(false);
85         },
86         /**
87          * Key up hander for the search text box.
88          * @param {Y.Event} e the keyup event.
89          */
90         handle_keyup : function(e) {
91             //  Trigger an ajax search after a delay.
92             this.cancel_timeout();
93             this.timeoutid = Y.later(this.querydelay*1000, e, function(obj){obj.send_query(false)}, this);
95             // Enable or diable the clear button.
96             this.clearbutton.set('disabled', (this.get_search_text() == ''));
98             // If enter was pressed, prevent a form submission from happening.
99             if (e.keyCode == 13) {
100                 e.halt();
101             }
102         },
103         /**
104          * Handles when the selection has changed. If the selection has changed from
105          * empty to not-empty, or vice versa, then fire the event handlers.
106          */
107         handle_selection_change : function() {
108             var isselectionempty = this.is_selection_empty();
109             if (isselectionempty !== this.selectionempty) {
110                 this.fire('user_selector:selectionchanged', isselectionempty);
111             }
112             this.selectionempty = isselectionempty;
113         },
114         /**
115          * Trigger a re-search when the 'search any substring' option is changed.
116          */
117         handle_searchanywhere_change : function() {
118             if (this.lastsearch != '' && this.get_search_text() != '') {
119                 this.send_query(true);
120             }
121         },
122         /**
123          * Click handler for the clear button..
124          */
125         handle_clear : function() {
126             this.searchfield.set('value', '');
127             this.clearbutton.set('disabled',true);
128             this.send_query(false);
129         },
130         /**
131          * Fires off the ajax search request.
132          */
133         send_query : function(forceresearch) {
134             // Cancel any pending timeout.
135             this.cancel_timeout();
137             var value = this.get_search_text();
138             this.searchfield.set('class', '');
139             if (this.lastsearch == value && !forceresearch) {
140                 return;
141             }
143             Y.io(M.cfg.wwwroot + '/user/selector/search.php', {
144                 method: 'POST',
145                 data: 'selectorid='+hash+'&sesskey='+M.cfg.sesskey+'&search='+value + '&userselector_searchanywhere=' + this.get_option('searchanywhere'),
146                 on: {
147                     success:this.handle_response,
148                     failure:this.handle_failure
149                 },
150                 context:this
151             });
153             this.lastsearch = value;
154             this.listbox.setStyle('background','url(' + M.util.image_url('i/loading', 'moodle') + ') no-repeat center center');
155         },
156         /**
157          * Handle what happens when we get some data back from the search.
158          * @param {int} requestid not used.
159          * @param {object} response the list of users that was returned.
160          */
161         handle_response : function(requestid, response) {
162             try {
163                 this.listbox.setStyle('background','');
164                 var data = Y.JSON.parse(response.responseText);
165                 this.output_options(data);
166             } catch (e) {
167                 this.handle_failure();
168             }
169         },
170         /**
171          * Handles what happens when the ajax request fails.
172          */
173         handle_failure : function() {
174             this.listbox.setStyle('background','');
175             this.searchfield.addClass('error');
177             // If we are in developer debug mode, output a link to help debug the failure.
178             if (M.cfg.developerdebug) {
179                 this.searchfield.insert(Y.Node.create('<a href="'+M.cfg.wwwroot +'/user/selector/search.php?selectorid='+hash+'&sesskey='+M.cfg.sesskey+'&search='+this.get_search_text()+'&debug=1">Ajax call failed. Click here to try the search call directly.</a>'));
180             }
181         },
182         /**
183          * This method should do the same sort of thing as the PHP method
184          * user_selector_base::output_options.
185          * @param {object} data the list of users to populate the list box with.
186          */
187         output_options : function(data) {
188             // Clear out the existing options, keeping any ones that are already selected.
189             var selectedusers = {};
190             this.listbox.all('optgroup').each(function(optgroup){
191                 optgroup.all('option').each(function(option){
192                     if (option.get('selected')) {
193                         selectedusers[option.get('value')] = {
194                             id : option.get('value'),
195                             name : option.get('innerText') || option.get('textContent'),
196                             disabled: option.get('disabled')
197                         }
198                     }
199                     option.remove();
200                 }, this);
201                 optgroup.remove();
202             }, this);
204             // Output each optgroup.
205             var count = 0;
206             for (var groupname in data.results) {
207                 this.output_group(groupname, data.results[groupname], selectedusers, true);
208                 count++;
209             }
210             if (!count) {
211                 var searchstr = (this.lastsearch != '')?this.insert_search_into_str(M.str.moodle.nomatchingusers, this.lastsearch):M.str.moodle.none;
212                 this.output_group(searchstr, {}, selectedusers, true)
213             }
215             // If there were previously selected users who do not match the search, show them too.
216             if (this.get_option('preserveselected') && selectedusers) {
217                 this.output_group(this.insert_search_into_str(M.str.moodle.previouslyselectedusers, this.lastsearch), selectedusers, true, false);
218             }
219             this.handle_selection_change();
220         },
221         /**
222          * This method should do the same sort of thing as the PHP method
223          * user_selector_base::output_optgroup.
224          *
225          * @param {string} groupname the label for this optgroup.v
226          * @param {object} users the users to put in this optgroup.
227          * @param {boolean|object} selectedusers if true, select the users in this group.
228          * @param {boolean} processsingle
229          */
230         output_group : function(groupname, users, selectedusers, processsingle) {
231             var optgroup = Y.Node.create('<optgroup></optgroup>');
232             var count = 0;
233             for (var userid in users) {
234                 var user = users[userid];
235                 var option = Y.Node.create('<option value="'+userid+'">'+user.name+'</option>');
236                 if (user.disabled) {
237                     option.set('disabled', true);
238                 } else if (selectedusers===true || selectedusers[userid]) {
239                     option.set('selected', true);
240                 } else {
241                     option.set('selected', false);
242                 }
243                 optgroup.append(option);
244                 count++;
245             }
247             if (count > 0) {
248                 optgroup.set('label', groupname+' ('+count+')');
249                 if (processsingle && count===1 && this.get_option('autoselectunique') && option.get('disabled')) {
250                     option.set('selected', true);
251                 }
252             } else {
253                 optgroup.append(Y.Node.create('<option disabled="disabled">\u00A0</option>'));
254             }
255             this.listbox.append(optgroup);
256         },
257         /**
258          * Replace
259          * @param {string} str
260          * @param {string} search The search term
261          * @return string
262          */
263         insert_search_into_str : function(str, search) {
264             return str.replace("%%SEARCHTERM%%", search);
265         },
266         /**
267          * Gets the search text
268          * @return String the value to search for, with leading and trailing whitespace trimmed.
269          */
270         get_search_text : function() {
271             return this.searchfield.get('value').toString().replace(/^ +| +$/, '');
272         },
273         /**
274          * Returns true if the selection is empty (nothing is selected)
275          * @return Boolean check all the options and return whether any are selected.
276          */
277         is_selection_empty : function() {
278             var selection = false;
279             this.listbox.all('option').each(function(){
280                 if (this.get('selected')) {
281                     selection = true;
282                 }
283             });
284             return !(selection);
285         },
286         /**
287          * Cancel the search delay timeout, if there is one.
288          */
289         cancel_timeout : function() {
290             if (this.timeoutid) {
291                 clearTimeout(this.timeoutid);
292                 this.timeoutid = null;
293             }
294         },
295         /**
296          * @param {string} name The name of the option to retrieve
297          * @return the value of one of the option checkboxes.
298          */
299         get_option : function(name) {
300             var checkbox = Y.one('#userselector_' + name + 'id');
301             if (checkbox) {
302                 return (checkbox.get('checked'));
303             } else {
304                 return false;
305             }
306         }
307     };
308     // Augment the user selector with the EventTarget class so that we can use
309     // custom events
310     Y.augment(user_selector, Y.EventTarget, null, null, {});
311     // Initialise the user selector
312     user_selector.init();
313     // Store the user selector so that it can be retrieved
314     this.user_selectors[name] = user_selector;
315     // Return the user selector
316     return user_selector;
317 };
319 /**
320  * Initialise a class that updates the user's preferences when they change one of
321  * the options checkboxes.
322  * @constructor
323  * @param {YUI} Y
324  * @return Tracker object
325  */
326 M.core_user.init_user_selector_options_tracker = function(Y) {
327     // Create a user selector options tracker
328     var user_selector_options_tracker = {
329         /**
330          * Initlises the option tracker and gets everything going.
331          * @constructor
332          */
333         init : function() {
334             var settings = [
335                 'userselector_preserveselected',
336                 'userselector_autoselectunique',
337                 'userselector_searchanywhere'
338             ];
339             for (var s in settings) {
340                 var setting = settings[s];
341                 Y.one('#'+setting+'id').on('click', this.set_user_preference, this, setting);
342             }
343         },
344         /**
345          * Sets a user preference for the options tracker
346          * @param {Y.Event|null} e
347          * @param {string} name The name of the preference to set
348          */
349         set_user_preference : function(e, name) {
350             M.util.set_user_preference(name, Y.one('#'+name+'id').get('checked'));
351         }
352     };
353     // Initialise the options tracker
354     user_selector_options_tracker.init();
355     // Return it just incase it is ever wanted
356     return user_selector_options_tracker;
357 };