user selection: MDL-16996 Improve the user selector used on the assign roles and group memebers pages - Write the JavaScript to do the Ajax requests and update the list of users.

This commit is contained in:
tjhunt 2008-10-28 06:51:36 +00:00
parent b61d8ee50d
commit d56f9e659d
5 changed files with 356 additions and 24 deletions

View File

@ -316,6 +316,7 @@ $string['modulemissingcode'] = 'Module $a is missing the code needed to perform
$string['modulerequirementsnotmet'] = 'Module \"$a->modulename\" ($a->moduleversion) could not be installed. It requires a newer version of Moodle (currently you are using $a->currentmoodle, you need $a->requiremoodle).';
$string['mustbeteacher'] = 'You must be a teacher to look at this page';
$string['multiplerestorenotallow'] = 'Multiple restore execution not allowed!';
$string['mustbeloggedin'] = 'You must be logged in to do this';
$string['needphpext'] = 'You need to add $a support to your PHP installation';
$string['needcopy'] = 'You need to copy something first!';
$string['needcoursecategroyid'] = 'Either course id or category must be specified';
@ -410,6 +411,7 @@ $string['unknownhelp'] = 'Unknown help topic $a';
$string['unknowngroup'] = 'Unknown group \"$a\"';
$string['unknownrole'] = 'Unknown role \"$a\"';
$string['unknownuseraction'] = 'Sorry, I do not understand this user action';
$string['unknownuserselector'] = 'Unknown user selector';
$string['unknownmodulename'] = 'Unknown module name for form';
$string['unknoworder'] = 'Unknown ordering';
$string['unknowparamtype'] = 'Unknown parameter type: $a';

View File

@ -123,9 +123,6 @@ abstract class user_selector_base {
public function display($return = false) {
global $USER, $CFG;
// Ensure that the list of previously selected users is up to date.
$this->get_selected_users();
// Get the list of requested users, and if there is only one, set a flag to autoselect it.
$search = optional_param($this->name . '_searchtext', '', PARAM_RAW);
$groupedusers = $this->find_users($search);
@ -146,12 +143,9 @@ abstract class user_selector_base {
$output = '<div class="userselector" id="' . $this->name . '_wrapper">' . "\n" .
'<select name="' . $name . '" id="' . $this->name . '" ' .
$multiselect . 'size="' . $this->rows . '">' . "\n";
foreach ($groupedusers as $groupname => $users) {
$output .= $this->output_optgroup($groupname, $users, $select);
}
if (!empty($this->selected)) {
$output .= $this->output_optgroup(get_string('previouslyselectedusers'), $this->selected, true);
}
// Populate the select.
$output .= $this->output_options($groupedusers, $select);
// Output the search controls.
$output .= "</select>\n<div>\n";
@ -161,16 +155,8 @@ abstract class user_selector_base {
$this->name . '_searchbutton" value="' . $this->search_button_caption() . '" />';
$output .= "</div>\n</div>\n\n";
// This method trashes $this->selected, so reset it so if someone tries to
// Use it again, it is rebuilt.
$this->selected = null;
// Put the options into the session for the benefit of the ajax code.
$options = $this->get_options();
$hash = md5(serialize($options));
$USER->userselectors[$hash] = $options;
$output .= '<p><a href="' . $CFG->wwwroot . '/user/selector/search.php?selectorid=' .
$hash . '&amp;' . 'sesskey=' . sesskey() . '&amp;search=">Ajax search script</a></p>'; // DONOTCOMMIT
// Initialise the ajax functionality.
$output .= $this->initialise_javascript();
// Return or output it.
if ($return) {
@ -323,6 +309,38 @@ abstract class user_selector_base {
return array(implode(' AND ', $tests), $params);
}
/**
* Output the list of <optgroup>s and <options>s that go inside the select.
* This method should do the same as the JavaScript method
* user_selector.prototype.handle_response.
*
* @param unknown_type $groupedusers
* @param unknown_type $select
* @return unknown
*/
protected function output_options($groupedusers, $select) {
$output = '';
// Ensure that the list of previously selected users is up to date.
$this->get_selected_users();
// Output each optgroup.
foreach ($groupedusers as $groupname => $users) {
$output .= $this->output_optgroup($groupname, $users, $select);
}
// If there were previously selected users who do not match the search, show them too.
if (!empty($this->selected)) {
$output .= $this->output_optgroup(get_string('previouslyselectedusers'), $this->selected, true);
}
// This method trashes $this->selected, so clear the cache so it is
// rebuilt before anyone tried to use it again.
$this->selected = null;
return $output;
}
protected function output_optgroup($groupname, $users, $select) {
$output = '<optgroup label="' . s($groupname) . ' (' . count($users) . ')">' . "\n";
if (!empty($users)) {
@ -344,10 +362,10 @@ abstract class user_selector_base {
}
/**
* Convert a user object to a string suitable for displaying as an option in the dropdown.
* Convert a user object to a string suitable for displaying as an option in the list box.
*
* @param object $user the user to display.
* @return string a string representation to display.
* @return string a string representation of the user.
*/
protected function output_user($user) {
$bits = array(
@ -365,9 +383,38 @@ abstract class user_selector_base {
protected function search_button_caption() {
return get_string('search');
}
/**
* Enter description here...
*
*/
protected function initialise_javascript() {
global $USER;
$output = '';
// Required JavaScript code.
require_js(array('yui_yahoo', 'yui_event', 'yui_json', 'yui_connection', 'yui_datasource'));
require_js('user/selector/script.js');
// Put the options into the session, to allow search.php to respond to the ajax requests.
$options = $this->get_options();
$hash = md5(serialize($options));
$USER->userselectors[$hash] = $options;
// Initialise the selector.
$output .= print_js_call('new user_selector', array($this->name, $hash,
sesskey(), $this->extrafields, get_string('previouslyselectedusers')), true);
return $output;
}
}
class role_assign_user_selector extends user_selector_base {
class role_assign_potential_user_selector extends user_selector_base {
public function find_users($search) {
return array(); // TODO
}
}
class role_assign_current_user_selector extends user_selector_base {
public function find_users($search) {
return array(); // TODO
}

270
user/selector/script.js Normal file
View File

@ -0,0 +1,270 @@
// 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
/**
*
* @constructor
*/
function user_selector(name, hash, sesskey, extrafields, strprevselected) {
this.name = name;
this.extrafields = extrafields;
this.strprevselected = strprevselected;
// Set up the data source.
this.datasource = new YAHOO.util.XHRDataSource(moodle_cfg.wwwroot +
'/user/selector/search.php?selectorid=' + hash + '&sesskey=' + sesskey + '&search=');
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.for = 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.
var oself = this;
YAHOO.util.Event.addListener(this.searchfield, "keyup", function(e) { oself.handle_keyup() });
this.lastsearch = this.get_search_text();
}
/**
* 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 = [];
/**
* The datasource used to fetch lists of users from Moodle.
* @property datasource
* @type YAHOO.widget.DataSource
*/
user_selector.prototype.datasource = null;
/**
* 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.2;
/**
* The input element that contains the search term.
*
* @property searchfield
* @type HTMLInputElement
*/
user_selector.prototype.searchfield = 0.2;
/**
* 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.
*
* @property lastsearch
* @type String
*/
user_selector.prototype.lastsearch = null;
/**
* Name of the previously selected users group.
*
* @property strprevselected
* @type String
*/
user_selector.prototype.strprevselected = '';
/**
* Used to track whether there is only one optoin matchin the search results, if
* so, it is automatically selected.
*
* @property strprevselected
* @type Object
**/
user_selector.prototype.onlyoption = null;
/**
* Key up hander for the search text box. Trigger an ajax search after a delay.
*/
user_selector.prototype.handle_keyup = function() {
if (this.timeoutid) {
clearTimeout(this.timeoutid);
this.timeoutid = null;
}
var oself = this;
this.timeoutid = setTimeout(function() { oself.send_query() }, this.querydelay * 1000);
}
/**
* @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(/^ +| +$/, '');
}
/**
* Fires off the ajax search request.
*/
user_selector.prototype.send_query = function() {
var value = this.get_search_text();
if (this.lastsearch == value) {
return;
}
this.datasource.sendRequest(this.searchfield.value, {
success: this.handle_response,
failure: this.handle_failure,
scope: this
});
this.lastsearch = value;
this.listbox.style.background = 'url(' + moodle_cfg.pixpath + '/i/loading.gif) 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 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');
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, formatted: optiontext };
}
optgroup.removeChild(option);
}
this.listbox.removeChild(optgroup);
}
var results = data.results[0];
// Output each optgroup.
this.onlyoption = null;
for (groupname in results) {
this.output_group(groupname, results[groupname], false);
}
// If there was only one option matching the search results, select it.
if (this.onlyoption) {
this.onlyoption.selected = true;
}
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 (areprevselected) {
this.output_group(this.strprevselected, this.selected, true);
}
this.selected = null;
}
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(this.output_user(user)));
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) {
var option = document.createElement('option');
option.disabled = 'disabled';
option.appendChild(document.createTextNode('\u00A0'));
optgroup.appendchild(option);
}
optgroup.label += ' (' + count + ')';
this.listbox.appendChild(optgroup);
}
/**
* Convert a user object to a string suitable for displaying as an option in the list box.
*
* @param Object user the user to display.
* @return string a string representation of the user.
*/
user_selector.prototype.output_user = function(user) {
if (user.formatted) {
return user.formatted;
}
var output = user.fullname;
for (var i = 0; i < this.extrafields.length; i++) {
output += ', ' + user[this.extrafields[i]];
}
return output;
}

View File

@ -34,7 +34,9 @@ require_once(dirname(__FILE__) . '/../../config.php');
require_once($CFG->dirroot . '/user/selector/lib.php');
// Check access.
require_login();
if (!isloggedin()) {;
print_error('mustbeloggedin');
}
if (!confirm_sesskey()) {
print_error('invalidsesskey');
}
@ -62,5 +64,13 @@ $userselector = new $classname($name, $options);
// Do the search and output the results.
$users = $userselector->find_users($search);
foreach ($users as &$group) {
foreach ($group as &$user) {
$user->fullname = fullname($user);
}
}
header('Content-type: application/json');
echo json_encode(array('results' => $users));
?>

View File

@ -14,7 +14,8 @@ class test_user_selector extends user_selector_base {
list($wherecondition, $params) = $this->search_sql($search, 'u');
$sql = 'SELECT ' . $this->required_fields_sql('u') .
' FROM {user} u' .
' WHERE ' . $wherecondition;
' WHERE ' . $wherecondition .
' ORDER BY u.lastname, u.firstname';
$users = $DB->get_recordset_sql($sql, $params);
$groupedusers = array();
if ($search) {
@ -40,6 +41,8 @@ if ($justdefineclass) {
return;
}
require_login();
require_capability('moodle/site:config', get_context_instance(CONTEXT_SYSTEM));
print_header();
$userselector = new test_user_selector('myuserselector');