mirror of
https://github.com/moodle/moodle.git
synced 2025-01-19 14:27:22 +01:00
520 lines
17 KiB
JavaScript
520 lines
17 KiB
JavaScript
// JavaScript for the user selectors.
|
|
// This is somewhat inspired by the autocomplete component in YUI.
|
|
// license: http://www.gnu.org/copyleft/gpl.html GNU Public License
|
|
// package: userselector
|
|
|
|
/**
|
|
* Initialise a new user selector.
|
|
* @constructor
|
|
* @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 label used for the optgroup of users who are selected but who do not match the current search.
|
|
*/
|
|
function user_selector(name, hash, extrafields, lastsearch, strprevselected, strnomatchingusers, strnone, iconloading) {
|
|
this.name = name;
|
|
this.extrafields = extrafields;
|
|
this.strprevselected = strprevselected;
|
|
this.strnomatchingusers = strnomatchingusers;
|
|
this.strnone = strnone;
|
|
this.iconloading = iconloading;
|
|
this.searchurl = M.cfg.wwwroot + '/user/selector/search.php?selectorid=' +
|
|
hash + '&sesskey=' + M.cfg.sesskey + '&search='
|
|
|
|
// Set up the data source.
|
|
this.datasource = new YAHOO.util.XHRDataSource(this.searchurl);
|
|
this.datasource.connXhrMode = 'cancelStaleRequests';
|
|
this.datasource.responseType = YAHOO.util.XHRDataSource.TYPE_JSON;
|
|
this.datasource.responseSchema = {resultsList: 'results'};
|
|
|
|
// Find some key HTML elements.
|
|
this.searchfield = document.getElementById(this.name + '_searchtext');
|
|
this.listbox = document.getElementById(this.name);
|
|
|
|
// Hide the search button and replace it with a label.
|
|
var searchbutton = document.getElementById(this.name + '_searchbutton');
|
|
var label = document.createElement('label');
|
|
label.htmlFor = this.name + '_searchtext';
|
|
label.appendChild(document.createTextNode(searchbutton.value));
|
|
this.searchfield.parentNode.insertBefore(label, this.searchfield);
|
|
searchbutton.parentNode.removeChild(searchbutton);
|
|
|
|
// Hook up the event handler for when the search text changes.
|
|
var oself = this;
|
|
YAHOO.util.Event.addListener(this.searchfield, "keyup", function(e) { oself.handle_keyup(e) });
|
|
this.lastsearch = lastsearch;
|
|
|
|
// Define our custom event.
|
|
this.createEvent('selectionchanged');
|
|
this.selectionempty = this.is_selection_empty();
|
|
user_selector.allselectors[name] = this;
|
|
|
|
// Hook up the event handler for when the selection changes.
|
|
YAHOO.util.Event.addListener(this.listbox, "keyup", function(e) { oself.handle_selection_change() });
|
|
YAHOO.util.Event.addListener(this.listbox, "click", function(e) { oself.handle_selection_change() });
|
|
YAHOO.util.Event.addListener(this.listbox, "change", function(e) { oself.handle_selection_change() });
|
|
|
|
// And when the search any substring preference changes. Do an immediate re-search.
|
|
YAHOO.util.Event.addListener('userselector_searchanywhereid', 'click', function(e) { oself.handle_searchanywhere_change() });
|
|
|
|
// Replace the Clear submit button with a clone that is not a submit button.
|
|
var oldclearbutton = document.getElementById(this.name + '_clearbutton');
|
|
this.clearbutton = document.createElement('input');
|
|
this.clearbutton.type = 'button';
|
|
this.clearbutton.value = oldclearbutton.value;
|
|
this.clearbutton.id = oldclearbutton.id;
|
|
oldclearbutton.id = '';
|
|
oldclearbutton.parentNode.insertBefore(this.clearbutton, oldclearbutton);
|
|
oldclearbutton.parentNode.removeChild(oldclearbutton);
|
|
|
|
// Enable or diable the clear button.
|
|
this.clearbutton.disabled = this.get_search_text() == '';
|
|
|
|
// Hook up the event handler for the clear button.
|
|
YAHOO.util.Event.addListener(this.clearbutton, "click", function(e) { oself.handle_clear() });
|
|
|
|
// If the contents of the search box is different from the search that was
|
|
// done on the server, reload the options. (This happens, for example,
|
|
// in Firefox. Go to the role assign page - the search box will be blank.
|
|
// Type something in the search box. Then click reload. The text will stay
|
|
// in the search box, but the server does not know about the new search term,
|
|
// so without this line, the list would contain all the users.
|
|
this.send_query(false);
|
|
}
|
|
|
|
/**
|
|
* A list of all the user_selectors on this page. Use by user_selector.get.
|
|
*
|
|
* @property allselectors
|
|
* @type Object
|
|
*/
|
|
user_selector.allselectors = {};
|
|
|
|
/**
|
|
* @param String name the name of a user selector on this page.
|
|
* @return user_selector the named user selector.
|
|
*/
|
|
user_selector.get = function(name) {
|
|
return user_selector.allselectors[name];
|
|
}
|
|
|
|
// Fields set be the constructor ===============================================
|
|
|
|
/**
|
|
* This id/name used for this control in the HTML.
|
|
* @property name
|
|
* @type String
|
|
*/
|
|
user_selector.prototype.name = null;
|
|
|
|
/**
|
|
* Array of fields to display for each user, in addition to fullname.
|
|
* @property extrafields
|
|
* @type Array
|
|
*/
|
|
user_selector.prototype.extrafields = [];
|
|
|
|
/**
|
|
* Name of the previously selected users group.
|
|
*
|
|
* @property strprevselected
|
|
* @type String
|
|
*/
|
|
user_selector.prototype.strprevselected = '';
|
|
|
|
/**
|
|
* Name of the no matching users group.
|
|
*
|
|
* @property strnomatchingusers
|
|
* @type String
|
|
*/
|
|
user_selector.prototype.strnomatchingusers = '';
|
|
|
|
/**
|
|
* Name of the no matching users group when empty.
|
|
*
|
|
* @property strnone
|
|
* @type String
|
|
*/
|
|
user_selector.prototype.strnone = '';
|
|
|
|
/**
|
|
* URL of the loading icon.
|
|
*
|
|
* @property iconloading
|
|
* @type String
|
|
*/
|
|
user_selector.prototype.iconloading = '';
|
|
|
|
// Fields that configure the control's behaviour ===============================
|
|
|
|
/**
|
|
* Number of seconds to delay before submitting a query request. If a query
|
|
* request is received before a previous one has completed its delay, the
|
|
* previous request is cancelled and the new request is set to the delay.
|
|
*
|
|
* @property querydelay
|
|
* @type Number
|
|
* @default 0.2
|
|
*/
|
|
user_selector.prototype.querydelay = 0.5;
|
|
|
|
// Internal fields =============================================================
|
|
|
|
/**
|
|
* The URL for the datasource.
|
|
* @property searchurl
|
|
* @type String
|
|
*/
|
|
user_selector.prototype.searchurl = null;
|
|
|
|
/**
|
|
* The datasource used to fetch lists of users from Moodle.
|
|
* @property datasource
|
|
* @type YAHOO.widget.DataSource
|
|
*/
|
|
user_selector.prototype.datasource = null;
|
|
|
|
/**
|
|
* The input element that contains the search term.
|
|
*
|
|
* @property searchfield
|
|
* @type HTMLInputElement
|
|
*/
|
|
user_selector.prototype.searchfield = null;
|
|
|
|
/**
|
|
* The clear button.
|
|
*
|
|
* @property clearbutton
|
|
* @type HTMLInputElement
|
|
*/
|
|
user_selector.prototype.clearbutton = null;
|
|
|
|
/**
|
|
* The select element that contains the list of users.
|
|
*
|
|
* @property listbox
|
|
* @type HTMLSelectElement
|
|
*/
|
|
user_selector.prototype.listbox = null;
|
|
|
|
/**
|
|
* Used to hold the timeout id of the timeout that waits before doing a search.
|
|
*
|
|
* @property timeoutid
|
|
* @type Number
|
|
*/
|
|
user_selector.prototype.timeoutid = null;
|
|
|
|
/**
|
|
* The last string that we searched for, so we can avoid unnecessary repeat searches.
|
|
*
|
|
* @property lastsearch
|
|
* @type String
|
|
*/
|
|
user_selector.prototype.lastsearch = null;
|
|
|
|
/**
|
|
* Used while the list of options is being refreshed, to track options that were
|
|
* selected before, so they are not lost if they do not appear in the search results.
|
|
*
|
|
* @property selected
|
|
* @type Object
|
|
*/
|
|
user_selector.prototype.selected = null;
|
|
|
|
/**
|
|
* Used while the list of options is being refreshed to determine if there is only
|
|
* one user matching the search, so they can be automatically selected.
|
|
*
|
|
* @property onlyoption
|
|
* @type HTMLOptionElement
|
|
**/
|
|
user_selector.prototype.onlyoption = null;
|
|
|
|
/**
|
|
* Whether any options where selected last time we checked. Used by
|
|
* handle_selection_change to track when this status changes.
|
|
*
|
|
* @property selectionempty
|
|
* @type Boolean
|
|
*/
|
|
user_selector.prototype.selectionempty = true;
|
|
|
|
// Methods for handing various events ==========================================
|
|
|
|
/**
|
|
* Key up hander for the search text box.
|
|
* @param object e the keyup event.
|
|
*/
|
|
user_selector.prototype.handle_keyup = function(e) {
|
|
// Trigger an ajax search after a delay.
|
|
this.cancel_timeout();
|
|
var oself = this;
|
|
this.timeoutid = setTimeout(function() { oself.send_query(false) }, this.querydelay * 1000);
|
|
|
|
// Enable or diable the clear button.
|
|
this.clearbutton.disabled = this.get_search_text() == '';
|
|
|
|
// If enter was pressed, prevent a form submission from happening.
|
|
var keyCode = e.keyCode ? e.keyCode : e.which;
|
|
if (keyCode == 13) {
|
|
YAHOO.util.Event.stopEvent(e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cancel the search delay timeout, if there is one.
|
|
*/
|
|
user_selector.prototype.cancel_timeout = function() {
|
|
if (this.timeoutid) {
|
|
clearTimeout(this.timeoutid);
|
|
this.timeoutid = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Click handler for the clear button..
|
|
*/
|
|
user_selector.prototype.handle_clear = function() {
|
|
this.searchfield.value = '';
|
|
this.clearbutton.disabled = true;
|
|
this.send_query(false);
|
|
}
|
|
|
|
/**
|
|
* Trigger a re-search when the 'search any substring' option is changed.
|
|
*/
|
|
user_selector.prototype.handle_searchanywhere_change = function() {
|
|
if (this.lastsearch != '' && this.get_search_text() != '') {
|
|
this.send_query(true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return String the value to search for, with leading and trailing whitespace trimmed.
|
|
*/
|
|
user_selector.prototype.get_search_text = function() {
|
|
return this.searchfield.value.replace(/^ +| +$/, '');
|
|
}
|
|
|
|
/**
|
|
* @return the value of one of the option checkboxes.<b>
|
|
*/
|
|
user_selector.prototype.get_option = function(name) {
|
|
var checkbox = document.getElementById('userselector_' + name + 'id');
|
|
if (checkbox) {
|
|
return checkbox.checked;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fires off the ajax search request.
|
|
*/
|
|
user_selector.prototype.send_query = function(forceresearch) {
|
|
// Cancel any pending timeout.
|
|
this.cancel_timeout();
|
|
|
|
var value = this.get_search_text();
|
|
this.searchfield.className = '';
|
|
if (this.lastsearch == value && !forceresearch) {
|
|
return;
|
|
}
|
|
this.datasource.sendRequest(value + '&userselector_searchanywhere=' + this.get_option('searchanywhere'), {
|
|
success: this.handle_response,
|
|
failure: this.handle_failure,
|
|
scope: this
|
|
});
|
|
this.lastsearch = value;
|
|
this.listbox.style.background = 'url(' + this.iconloading + ') no-repeat center center';
|
|
}
|
|
|
|
/**
|
|
* Handle what happens when we get some data back from the search.
|
|
* @param Object request not used.
|
|
* @param Object data the list of users that was returned.
|
|
*/
|
|
user_selector.prototype.handle_response = function(request, data) {
|
|
this.listbox.style.background = '';
|
|
this.output_options(data);
|
|
}
|
|
|
|
/**
|
|
* Handles what happens when the ajax request fails.
|
|
*/
|
|
user_selector.prototype.handle_failure = function() {
|
|
this.listbox.style.background = '';
|
|
this.searchfield.className = 'error';
|
|
|
|
// If we are in developer debug mode, output a link to help debug the failure.
|
|
if (M.cfg.developerdebug) {
|
|
var link = document.createElement('a');
|
|
link.href = this.searchurl + this.get_search_text() + '&debug=1';
|
|
link.appendChild(document.createTextNode('Ajax call failed. Click here to try the search call directly.'))
|
|
this.searchfield.parentNode.appendChild(link);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return Boolean check all the options and return whether any are selected.
|
|
*/
|
|
user_selector.prototype.is_selection_empty = function() {
|
|
var options = this.listbox.getElementsByTagName('option');
|
|
for (i = 0; i < options.length; i++) {
|
|
var option = options[i];
|
|
if (option.selected) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Handles when the selection has changed. If the selection has changed from
|
|
* empty to not-empty, or vice versa, then fire the event handlers.
|
|
*/
|
|
user_selector.prototype.handle_selection_change = function() {
|
|
var isselectionempty = this.is_selection_empty();
|
|
if (isselectionempty !== this.selectionempty) {
|
|
this.fireEvent('selectionchanged', isselectionempty);
|
|
}
|
|
this.selectionempty = isselectionempty;
|
|
}
|
|
|
|
// Methods for refreshing the list of displayed options ========================
|
|
user_selector.prototype.insert_search_into_str = function(string, search) {
|
|
return string.replace("%%SEARCHTERM%%", search);
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
user_selector.prototype.output_options = function(data) {
|
|
// Clear out the existing options, keeping any ones that are already selected.
|
|
this.selected = {};
|
|
var groups = this.listbox.getElementsByTagName('optgroup');
|
|
var preserveselected = this.get_option('preserveselected');
|
|
while (groups.length > 0) {
|
|
var optgroup = groups[0]; // Remeber that groups is a live array as we remove optgroups from the select, it updates.
|
|
var options = optgroup.getElementsByTagName('option');
|
|
while (options.length > 0) {
|
|
var option = options[0];
|
|
if (option.selected) {
|
|
var optiontext = option.innerText || option.textContent
|
|
this.selected[option.value] = { id: option.value, name: optiontext, disabled: option.disabled };
|
|
}
|
|
optgroup.removeChild(option);
|
|
}
|
|
this.listbox.removeChild(optgroup);
|
|
}
|
|
|
|
var results = data.results[0];
|
|
|
|
// Output each optgroup.
|
|
this.onlyoption = null;
|
|
var nogroups = true;
|
|
for (groupname in results) {
|
|
this.output_group(groupname, results[groupname], false);
|
|
nogroups = false;
|
|
}
|
|
|
|
if (nogroups) {
|
|
if (this.lastsearch != '') {
|
|
this.output_group(this.insert_search_into_str(this.strnomatchingusers, this.lastsearch), {}, false)
|
|
} else {
|
|
this.output_group(this.strnone, {}, false)
|
|
}
|
|
}
|
|
|
|
// If there was only one option matching the search results, select it.
|
|
if (this.get_option('autoselectunique') && this.onlyoption && !this.onlyoption.disabled) {
|
|
this.onlyoption.selected = true;
|
|
if (!this.listbox.multiple) {
|
|
this.selected = {};
|
|
}
|
|
this.handle_selection_change();
|
|
}
|
|
this.onlyoption = null;
|
|
|
|
// If there were previously selected users who do not match the search, show them too.
|
|
var areprevselected = false;
|
|
for (user in this.selected) {
|
|
areprevselected = true;
|
|
break;
|
|
}
|
|
if (preserveselected && areprevselected) {
|
|
this.output_group(this.insert_search_into_str(this.strprevselected, this.lastsearch), this.selected, true);
|
|
}
|
|
this.selected = null;
|
|
}
|
|
|
|
/**
|
|
* 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 select if true, select the users in this group.
|
|
*/
|
|
user_selector.prototype.output_group = function(groupname, users, select) {
|
|
var optgroup = document.createElement('optgroup');
|
|
optgroup.label = groupname;
|
|
var count = 0;
|
|
for (var userid in users) {
|
|
var user = users[userid];
|
|
var option = document.createElement('option');
|
|
option.value = user.id;
|
|
option.appendChild(document.createTextNode(user.name));
|
|
if (user.disabled) {
|
|
option.disabled = 'disabled';
|
|
} else if (select || this.selected[user.id]) {
|
|
option.selected = 'selected';
|
|
}
|
|
delete this.selected[user.id];
|
|
optgroup.appendChild(option);
|
|
if (this.onlyoption === null) {
|
|
this.onlyoption = option;
|
|
} else {
|
|
this.onlyoption = false;
|
|
}
|
|
count++;
|
|
}
|
|
if (count > 0) {
|
|
optgroup.label += ' (' + count + ')';
|
|
} else {
|
|
var option = document.createElement('option');
|
|
option.disabled = 'disabled';
|
|
option.appendChild(document.createTextNode('\u00A0'));
|
|
optgroup.appendChild(option);
|
|
}
|
|
this.listbox.appendChild(optgroup);
|
|
}
|
|
|
|
// Say that we want to be a source of custom events.
|
|
YAHOO.lang.augmentProto(user_selector, YAHOO.util.EventProvider);
|
|
|
|
/**
|
|
* Initialise a class that updates the user's preferences when they change one of
|
|
* the options checkboxes.
|
|
* @constructor
|
|
*/
|
|
function user_selector_options_tracker() {
|
|
var oself = this;
|
|
|
|
// Add event listeners.
|
|
YAHOO.util.Event.addListener('userselector_preserveselectedid', 'click',
|
|
function(e) { oself.handle_option_change('userselector_preserveselected') });
|
|
YAHOO.util.Event.addListener('userselector_autoselectuniqueid', 'click',
|
|
function(e) { oself.handle_option_change('userselector_autoselectunique') });
|
|
YAHOO.util.Event.addListener('userselector_searchanywhereid', 'click',
|
|
function(e) { oself.handle_option_change('userselector_searchanywhere') });
|
|
}
|
|
|
|
user_selector_options_tracker.prototype.handle_option_change = function(option) {
|
|
M.util.set_user_preference(option, document.getElementById(option + 'id').checked);
|
|
} |