moodle/user/selector/module.js
Tim Hunt c62488c22f MDL-35776 user ajax: fix sort order in chrome.
It seems that Chrome orders fields of objects in order of array key.
Therefore we must stop using user.id in the PHP arrays, and instead
ensure that we use sequential numbers.

This commit fixes the user selector.
2012-10-03 18:46:07 +01:00

382 lines
16 KiB
JavaScript

/**
* JavaScript for the user selectors.
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @package userselector
*/
// Define the core_user namespace if it has not already been defined
M.core_user = M.core_user || {};
// Define a user selectors array for against the cure_user namespace
M.core_user.user_selectors = [];
/**
* Retrieves an instantiated user selector or null if there isn't one by the requested name
* @param {string} name The name of the selector to retrieve
* @return bool
*/
M.core_user.get_user_selector = function (name) {
return this.user_selectors[name] || null;
};
/**
* Initialise a new user selector.
*
* @param {YUI} Y The YUI3 instance
* @param {string} name the control name/id.
* @param {string} hash the hash that identifies this selector in the user's session.
* @param {array} extrafields extra fields we are displaying for each user in addition to fullname.
* @param {string} lastsearch The last search that took place
*/
M.core_user.init_user_selector = function (Y, name, hash, extrafields, lastsearch) {
// Creates a new user_selector object
var user_selector = {
/** This id/name used for this control in the HTML. */
name : name,
/** Array of fields to display for each user, in addition to fullname. */
extrafields: extrafields,
/** Number of seconds to delay before submitting a query request */
querydelay : 0.5,
/** The input element that contains the search term. */
searchfield : Y.one('#'+name + '_searchtext'),
/** The clear button. */
clearbutton : null,
/** The select element that contains the list of users. */
listbox : Y.one('#'+name),
/** Used to hold the timeout id of the timeout that waits before doing a search. */
timeoutid : null,
/** Stores any in-progress remote requests. */
iotransactions : {},
/** The last string that we searched for, so we can avoid unnecessary repeat searches. */
lastsearch : lastsearch,
/** Whether any options where selected last time we checked. Used by
* handle_selection_change to track when this status changes. */
selectionempty : true,
/**
* Initialises the user selector object
* @constructor
*/
init : function() {
// Hide the search button and replace it with a label.
var searchbutton = Y.one('#'+this.name + '_searchbutton');
this.searchfield.insert(Y.Node.create('<label for="'+this.name + '_searchtext">'+searchbutton.get('value')+'</label>'), this.searchfield);
searchbutton.remove();
// Hook up the event handler for when the search text changes.
this.searchfield.on('keyup', this.handle_keyup, this);
// Hook up the event handler for when the selection changes.
this.listbox.on('keyup', this.handle_selection_change, this);
this.listbox.on('click', this.handle_selection_change, this);
this.listbox.on('change', this.handle_selection_change, this);
// And when the search any substring preference changes. Do an immediate re-search.
Y.one('#userselector_searchanywhereid').on('click', this.handle_searchanywhere_change, this);
// Define our custom event.
//this.createEvent('selectionchanged');
this.selectionempty = this.is_selection_empty();
// Replace the Clear submit button with a clone that is not a submit button.
var clearbtn = Y.one('#'+this.name + '_clearbutton');
this.clearbutton = Y.Node.create('<input type="button" value="'+clearbtn.get('value')+'" />');
clearbtn.replace(Y.Node.getDOMNode(this.clearbutton));
this.clearbutton.set('id', this.name+"_clearbutton");
this.clearbutton.on('click', this.handle_clear, this);
this.clearbutton.set('disabled', (this.get_search_text() == ''));
this.send_query(false);
},
/**
* Key up hander for the search text box.
* @param {Y.Event} e the keyup event.
*/
handle_keyup : function(e) {
// Trigger an ajax search after a delay.
this.cancel_timeout();
this.timeoutid = Y.later(this.querydelay*1000, e, function(obj){obj.send_query(false)}, this);
// Enable or diable the clear button.
this.clearbutton.set('disabled', (this.get_search_text() == ''));
// If enter was pressed, prevent a form submission from happening.
if (e.keyCode == 13) {
e.halt();
}
},
/**
* Handles when the selection has changed. If the selection has changed from
* empty to not-empty, or vice versa, then fire the event handlers.
*/
handle_selection_change : function() {
var isselectionempty = this.is_selection_empty();
if (isselectionempty !== this.selectionempty) {
this.fire('user_selector:selectionchanged', isselectionempty);
}
this.selectionempty = isselectionempty;
},
/**
* Trigger a re-search when the 'search any substring' option is changed.
*/
handle_searchanywhere_change : function() {
if (this.lastsearch != '' && this.get_search_text() != '') {
this.send_query(true);
}
},
/**
* Click handler for the clear button..
*/
handle_clear : function() {
this.searchfield.set('value', '');
this.clearbutton.set('disabled',true);
this.send_query(false);
},
/**
* Fires off the ajax search request.
*/
send_query : function(forceresearch) {
// Cancel any pending timeout.
this.cancel_timeout();
var value = this.get_search_text();
this.searchfield.set('class', '');
if (this.lastsearch == value && !forceresearch) {
return;
}
// Try to cancel existing transactions.
Y.Object.each(this.iotransactions, function(trans) {
trans.abort();
});
var iotrans = Y.io(M.cfg.wwwroot + '/user/selector/search.php', {
method: 'POST',
data: 'selectorid='+hash+'&sesskey='+M.cfg.sesskey+'&search='+value + '&userselector_searchanywhere=' + this.get_option('searchanywhere'),
on: {
success:this.handle_response,
failure:this.handle_failure
},
context:this
});
this.iotransactions[iotrans.id] = iotrans;
this.lastsearch = value;
this.listbox.setStyle('background','url(' + M.util.image_url('i/loading', 'moodle') + ') no-repeat center center');
},
/**
* Handle what happens when we get some data back from the search.
* @param {int} requestid not used.
* @param {object} response the list of users that was returned.
*/
handle_response : function(requestid, response) {
try {
delete this.iotransactions[requestid];
if (!Y.Object.isEmpty(this.iotransactions)) {
// More searches pending. Wait until they are all done.
return;
}
this.listbox.setStyle('background','');
var data = Y.JSON.parse(response.responseText);
this.output_options(data);
} catch (e) {
this.handle_failure(requestid);
}
},
/**
* Handles what happens when the ajax request fails.
*/
handle_failure : function(requestid) {
delete this.iotransactions[requestid];
if (!Y.Object.isEmpty(this.iotransactions)) {
// More searches pending. Wait until they are all done.
return;
}
this.listbox.setStyle('background','');
this.searchfield.addClass('error');
// If we are in developer debug mode, output a link to help debug the failure.
if (M.cfg.developerdebug) {
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>'));
}
},
/**
* This method should do the same sort of thing as the PHP method
* user_selector_base::output_options.
* @param {object} data the list of users to populate the list box with.
*/
output_options : function(data) {
// Clear out the existing options, keeping any ones that are already selected.
var selectedusers = {};
this.listbox.all('optgroup').each(function(optgroup){
optgroup.all('option').each(function(option){
if (option.get('selected')) {
selectedusers[option.get('value')] = {
id : option.get('value'),
name : option.get('innerText') || option.get('textContent'),
disabled: option.get('disabled')
}
}
option.remove();
}, this);
optgroup.remove();
}, this);
// Output each optgroup.
var count = 0;
for (var key in data.results) {
var groupdata = data.results[key];
this.output_group(groupdata.name, groupdata.users, selectedusers, true);
count++;
}
if (!count) {
var searchstr = (this.lastsearch != '')?this.insert_search_into_str(M.str.moodle.nomatchingusers, this.lastsearch):M.str.moodle.none;
this.output_group(searchstr, {}, selectedusers, true)
}
// If there were previously selected users who do not match the search, show them too.
if (this.get_option('preserveselected') && selectedusers) {
this.output_group(this.insert_search_into_str(M.str.moodle.previouslyselectedusers, this.lastsearch), selectedusers, true, false);
}
this.handle_selection_change();
},
/**
* This method should do the same sort of thing as the PHP method
* user_selector_base::output_optgroup.
*
* @param {string} groupname the label for this optgroup.v
* @param {object} users the users to put in this optgroup.
* @param {boolean|object} selectedusers if true, select the users in this group.
* @param {boolean} processsingle
*/
output_group : function(groupname, users, selectedusers, processsingle) {
var optgroup = Y.Node.create('<optgroup></optgroup>');
var count = 0;
for (var key in users) {
var user = users[key];
var option = Y.Node.create('<option value="'+user.id+'">'+user.name+'</option>');
if (user.disabled) {
option.set('disabled', true);
} else if (selectedusers===true || selectedusers[user.id]) {
option.set('selected', true);
delete selectedusers[user.id];
} else {
option.set('selected', false);
}
optgroup.append(option);
if (user.infobelow) {
extraoption = Y.Node.create('<option disabled="disabled" class="userselector-infobelow"/>');
extraoption.appendChild(document.createTextNode(user.infobelow));
optgroup.append(extraoption);
}
count++;
}
if (count > 0) {
optgroup.set('label', groupname+' ('+count+')');
if (processsingle && count===1 && this.get_option('autoselectunique') && option.get('disabled')) {
option.set('selected', true);
}
} else {
optgroup.append(Y.Node.create('<option disabled="disabled">\u00A0</option>'));
}
this.listbox.append(optgroup);
},
/**
* Replace
* @param {string} str
* @param {string} search The search term
* @return string
*/
insert_search_into_str : function(str, search) {
return str.replace("%%SEARCHTERM%%", search);
},
/**
* Gets the search text
* @return String the value to search for, with leading and trailing whitespace trimmed.
*/
get_search_text : function() {
return this.searchfield.get('value').toString().replace(/^ +| +$/, '');
},
/**
* Returns true if the selection is empty (nothing is selected)
* @return Boolean check all the options and return whether any are selected.
*/
is_selection_empty : function() {
var selection = false;
this.listbox.all('option').each(function(){
if (this.get('selected')) {
selection = true;
}
});
return !(selection);
},
/**
* Cancel the search delay timeout, if there is one.
*/
cancel_timeout : function() {
if (this.timeoutid) {
clearTimeout(this.timeoutid);
this.timeoutid = null;
}
},
/**
* @param {string} name The name of the option to retrieve
* @return the value of one of the option checkboxes.
*/
get_option : function(name) {
var checkbox = Y.one('#userselector_' + name + 'id');
if (checkbox) {
return (checkbox.get('checked'));
} else {
return false;
}
}
};
// Augment the user selector with the EventTarget class so that we can use
// custom events
Y.augment(user_selector, Y.EventTarget, null, null, {});
// Initialise the user selector
user_selector.init();
// Store the user selector so that it can be retrieved
this.user_selectors[name] = user_selector;
// Return the user selector
return user_selector;
};
/**
* Initialise a class that updates the user's preferences when they change one of
* the options checkboxes.
* @constructor
* @param {YUI} Y
* @return Tracker object
*/
M.core_user.init_user_selector_options_tracker = function(Y) {
// Create a user selector options tracker
var user_selector_options_tracker = {
/**
* Initlises the option tracker and gets everything going.
* @constructor
*/
init : function() {
var settings = [
'userselector_preserveselected',
'userselector_autoselectunique',
'userselector_searchanywhere'
];
for (var s in settings) {
var setting = settings[s];
Y.one('#'+setting+'id').on('click', this.set_user_preference, this, setting);
}
},
/**
* Sets a user preference for the options tracker
* @param {Y.Event|null} e
* @param {string} name The name of the preference to set
*/
set_user_preference : function(e, name) {
M.util.set_user_preference(name, Y.one('#'+name+'id').get('checked'));
}
};
// Initialise the options tracker
user_selector_options_tracker.init();
// Return it just incase it is ever wanted
return user_selector_options_tracker;
};