user selection: MDL-17073 add options to control the search in a collapsible region.

This commit is contained in:
tjhunt 2008-11-04 05:12:12 +00:00
parent 67c8a3e870
commit 904998d8f9
6 changed files with 162 additions and 29 deletions

View File

@ -1325,6 +1325,7 @@ $string['search'] = 'Search';
$string['searchagain'] = 'Search again';
$string['searchcourses'] = 'Search courses';
$string['searchhelp'] = 'You can search for multiple words at once.<br /><br />word : find any match of this word within the text.<br />+word : only exact matching words will be found.<br />-word : don\'t include results containing this word.';
$string['searchoptions'] = 'Search options';
$string['searchresults'] = 'Search results';
$string['sec'] = 'sec';
$string['secondstotime172800'] = '2 days';
@ -1593,6 +1594,9 @@ $string['usernotconfirmed'] = 'Could not confirm $a';
$string['userpic'] = 'User picture';
$string['userprofilefor'] = 'User profile for $a';
$string['users'] = 'Users';
$string['userselectorpreserveselected'] = 'Keep selected users, even if they no longer match the search';
$string['userselectorautoselectunique'] = 'If only one user matches the search, select them automatically';
$string['userselectorsearchanywhere'] = 'Match the search text anywhere in the user\'s name';
$string['usersnew'] = 'New users';
$string['usersnoaccesssince'] = 'Inactive for more than';
$string['userzones'] = 'User zones';

View File

@ -607,7 +607,7 @@ function collapsible_region(id, userpref, strtooltip) {
// Find the divs in the document.
this.div = document.getElementById(id);
this.innerdiv = document.getElementById(id + '_inner');
this.innerdiv = document.getElementById(id + '_sizer');
this.caption = document.getElementById(id + '_caption');
this.caption.title = strtooltip;
@ -641,8 +641,11 @@ function collapsible_region(id, userpref, strtooltip) {
a.appendChild(this.icon);
// Hook up the event handler.
self = this;
var self = this;
YAHOO.util.Event.addListener(a, 'click', function(e) {self.handle_click(e);});
// Handler for the animation finishing.
this.animation.onComplete.subscribe(function() {self.handle_animation_complete();});
}
/**
@ -681,6 +684,7 @@ collapsible_region.prototype.collapsed = false;
*/
collapsible_region.prototype.animation = null;
/** When clicked, toggle the collapsed state, and trigger the animation. */
collapsible_region.prototype.handle_click = function(e) {
// Toggle the state.
this.collapsed = !this.collapsed;
@ -694,7 +698,6 @@ collapsible_region.prototype.handle_click = function(e) {
}
if (this.collapsed) {
var targetel = this.caption;
this.div.className += ' collapsed';
} else {
var targetel = this.innerdiv;
this.div.className = this.div.className.replace(/\s*\bcollapsed\b\s*/, ' ');
@ -715,3 +718,10 @@ collapsible_region.prototype.handle_click = function(e) {
set_user_preference(this.userpref, this.collapsed);
}
}
/** When when the animation is finished, add the collapsed class name in relevant. */
collapsible_region.prototype.handle_animation_complete = function() {
if (this.collapsed) {
this.div.className += ' collapsed';
}
}

View File

@ -4166,7 +4166,7 @@ function print_collapsible_region_start($classes, $id, $caption, $userpref = fal
* @return mixed if $return is false, returns nothing, otherwise returns a string of HTML.
*/
function print_collapsible_region_end($return = false) {
$output = '</div></div>';
$output = '</div></div></div>';
if ($return) {
return $output;

View File

@ -226,6 +226,9 @@ div.collapsibleregion div.collapsibleregioncaption a {
color: inherit;
text-decoration: none;
}
.jsenabled .collapsed .collapsibleregioninner {
visibility: hidden;
}
.noticebox {
border-width:1px;
@ -570,7 +573,12 @@ div.hide {
.userselector div label {
margin-right: 0.3em;
}
#userselector_options {
font-size: 0.75em;
}
#userselector_options .collapsibleregioncaption {
font-weight: bold;
}
/***
*** Forms
***/
@ -1049,7 +1057,7 @@ body#admin-modules table.generaltable td.c0
width: 100%;
}
.roleassigntable td {
vertical-align: middle;
vertical-align: top;
padding: 0.2em 0.3em;
}
.roleassigntable p {
@ -1083,6 +1091,7 @@ body#admin-modules table.generaltable td.c0
font-weight: bold;
}
.roleassigntable #buttonscell #addcontrols {
margin-top: 3em;
height: 13em;
}
.roleassigntable #removeselect_wrapper,

View File

@ -55,10 +55,22 @@ abstract class user_selector_base {
protected $exclude = array();
/** A list of the users who are selected. */
protected $selected = null;
/** When the search changes, do we keep previously selected options that do
* not match the new search term? */
protected $preserveselected = false;
/** If only one user matches the search, should we select them automatically. */
protected $autoselectunique = false;
/** When searching, do we only match the starts of fields (better performace)
* or do we match occurrences anywhere? */
protected $searchanywhere = false;
// This is used by get selected users,
private $validatinguserids = null;
// Used to ensure we only output the search options for one user selector on
// each page.
private static $searchoptionsoutput = false;
// Public API ==============================================================
/**
@ -81,6 +93,9 @@ abstract class user_selector_base {
if (isset($options['exclude']) && is_array($options['exclude'])) {
$this->exclude = $options['exclude'];
}
$this->preserveselected = $this->initialise_option('userselector_preserveselected', $this->preserveselected);
$this->autoselectunique = $this->initialise_option('userselector_autoselectunique', $this->autoselectunique);
$this->searchanywhere = $this->initialise_option('userselector_searchanywhere', $this->searchanywhere);
}
/**
@ -166,10 +181,23 @@ abstract class user_selector_base {
$this->name . '_searchbutton" value="' . $this->search_button_caption() . '" />';
$output .= '<input type="submit" name="' . $this->name . '_clearbutton" id="' .
$this->name . '_clearbutton" value="' . get_string('clear') . '" />';
// And the search options.
$optionsoutput = false;
if (!user_selector_base::$searchoptionsoutput) {
$output .= print_collapsible_region_start('', 'userselector_options',
get_string('searchoptions'), 'userselector_optionscollapsed', true, true);
$output .= $this->option_checkbox('preserveselected', $this->preserveselected, get_string('userselectorpreserveselected'));
$output .= $this->option_checkbox('autoselectunique', $this->autoselectunique, get_string('userselectorautoselectunique'));
$output .= $this->option_checkbox('searchanywhere', $this->searchanywhere, get_string('userselectorsearchanywhere'));
$output .= print_collapsible_region_end(true);
user_selector_base::$searchoptionsoutput = true;
$optionsoutput = true;
}
$output .= "</div>\n</div>\n\n";
// Initialise the ajax functionality.
$output .= $this->initialise_javascript();
$output .= $this->initialise_javascript($optionsoutput);
// Return or output it.
if ($return) {
@ -345,9 +373,14 @@ abstract class user_selector_base {
$conditions[] = $u . $field;
}
$ilike = ' ' . $DB->sql_ilike() . ' ?';
if ($this->searchanywhere) {
$searchparam = '%' . $search . '%';
} else {
$searchparam = $search . '%';
}
foreach ($conditions as &$condition) {
$condition .= $ilike;
$params[] = $search . '%';
$params[] = $searchparam;
}
$tests[] = '(' . implode(' OR ', $conditions) . ')';
}
@ -393,12 +426,13 @@ abstract class user_selector_base {
// Ensure that the list of previously selected users is up to date.
$this->get_selected_users();
// If $groupedusers is empty, make a 'no matching users' group. If there
// is only one selected user, set a flag to select them.
// If $groupedusers is empty, make a 'no matching users' group. If there is
// only one selected user, set a flag to select them if that option is turned on.
$select = false;
if (empty($groupedusers)) {
$groupedusers = array(get_string('nomatchingusers', '', $search) => array());
} else if (count($groupedusers) == 1 && count(reset($groupedusers)) == 1) {
} else if ($this->autoselectunique && count($groupedusers) == 1 &&
count(reset($groupedusers)) == 1) {
$select = true;
if (!$this->multiselect) {
$this->selected = array();
@ -411,7 +445,7 @@ abstract class user_selector_base {
}
// If there were previously selected users who do not match the search, show them too.
if (!empty($this->selected)) {
if ($this->preserveselected && !empty($this->selected)) {
$output .= $this->output_optgroup(get_string('previouslyselectedusers', '', $search), $this->selected, true);
}
@ -475,12 +509,39 @@ abstract class user_selector_base {
return get_string('search');
}
// Initialise one of the option checkboxes, either from
// the request, or failing that from the user_preferences table, or
// finally from the given default.
private function initialise_option($name, $default) {
$param = optional_param($name, null, PARAM_BOOL);
if (is_null($param)) {
return get_user_preferences($name, $default);
} else {
set_user_preference($name, $param);
return $param;
}
}
// Output one of the options checkboxes.
private function option_checkbox($name, $on, $label) {
if ($on) {
$checked = ' checked="checked"';
} else {
$checked = '';
}
$name = 'userselector_' . $name;
$output = '<p><input type="hidden" name="' . $name . '" value="0" />' .
'<input type="checkbox" id="' . $name . '" name="' . $name . '" value="1"' . $checked . ' /> ' .
'<label for="' . $name . '">' . $label . "</label></p>\n";
user_preference_allow_ajax_update($name, PARAM_BOOL);
return $output;
}
/**
*
*
* @param boolean $optiontracker if true, initialise JavaScript for updating the user prefs.
* @return any HTML needed here.
*/
protected function initialise_javascript() {
protected function initialise_javascript($optiontracker) {
global $USER;
$output = '';
@ -495,8 +556,14 @@ abstract class user_selector_base {
// Initialise the selector.
$output .= print_js_call('new user_selector', array($this->name, $hash,
sesskey(), $this->extrafields, get_string('previouslyselectedusers', '', '%%SEARCHTERM%%'),
$this->extrafields, get_string('previouslyselectedusers', '', '%%SEARCHTERM%%'),
get_string('nomatchingusers', '', '%%SEARCHTERM%%')), true);
// Initialise the options tracker, if they are our responsibility.
if ($optiontracker) {
$output .= print_js_call('new user_selector_options_tracker', array(), true);
}
return $output;
}
}

View File

@ -8,17 +8,16 @@
* @constructor
* @param String name the control name/id.
* @param String hash the hash that identifies this selector in the user's session.
* @param String sesskey the user's sesskey.
* @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, sesskey, extrafields, strprevselected, strnomatchingusers) {
function user_selector(name, hash, extrafields, strprevselected, strnomatchingusers) {
this.name = name;
this.extrafields = extrafields;
this.strprevselected = strprevselected;
this.strnomatchingusers = strnomatchingusers;
this.searchurl = moodle_cfg.wwwroot + '/user/selector/search.php?selectorid=' +
hash + '&sesskey=' + sesskey + '&search='
hash + '&sesskey=' + moodle_cfg.sesskey + '&search='
// Set up the data source.
this.datasource = new YAHOO.util.XHRDataSource(this.searchurl);
@ -53,6 +52,9 @@ function user_selector(name, hash, sesskey, extrafields, strprevselected, strnom
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 research.
YAHOO.util.Event.addListener('userselector_searchanywhere', "click", function(e) { oself.handle_searchanywhere_click() });
// 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');
@ -129,7 +131,7 @@ user_selector.prototype.strnomatchingusers = '';
* @type Number
* @default 0.2
*/
user_selector.prototype.querydelay = 0.2;
user_selector.prototype.querydelay = 0.5;
// Internal fields =============================================================
@ -224,7 +226,7 @@ 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() }, this.querydelay * 1000);
this.timeoutid = setTimeout(function() { oself.send_query(false) }, this.querydelay * 1000);
// Enable or diable the clear button.
this.clearbutton.disabled = this.get_search_text() == '';
@ -247,12 +249,21 @@ user_selector.prototype.cancel_timeout = function() {
}
/**
* Key up hander for the search text box.
* Click handler for the clear button..
*/
user_selector.prototype.handle_clear = function() {
this.searchfield.value = '';
this.clearbutton.disabled = true;
this.send_query();
this.send_query(false);
}
/**
* Trigger a re-search when the 'search any substring' option is changed.
*/
user_selector.prototype.handle_searchanywhere_click = function() {
if (this.lastsearch != '' && this.get_search_text() != '') {
this.send_query(true);
}
}
/**
@ -262,19 +273,31 @@ 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);
if (checkbox) {
return checkbox.checked;
} else {
return false;
}
}
/**
* Fires off the ajax search request.
*/
user_selector.prototype.send_query = function() {
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) {
if (this.lastsearch == value && !forceresearch) {
return;
}
this.datasource.sendRequest(this.searchfield.value, {
this.datasource.sendRequest(value + '&userselector_searchanywhere=' + this.get_option('searchanywhere'), {
success: this.handle_response,
failure: this.handle_failure,
scope: this
@ -349,12 +372,13 @@ 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) {
if (preserveselected && option.selected) {
var optiontext = option.innerText || option.textContent
this.selected[option.value] = { id: option.value, name: optiontext, disabled: option.disabled };
}
@ -378,7 +402,7 @@ user_selector.prototype.output_options = function(data) {
}
// If there was only one option matching the search results, select it.
if (this.onlyoption && !this.onlyoption.disabled) {
if (this.get_option('autoselectunique') && this.onlyoption && !this.onlyoption.disabled) {
this.onlyoption.selected = true;
if (!this.listbox.multiple) {
this.selected = {};
@ -441,4 +465,23 @@ user_selector.prototype.output_group = function(groupname, users, select) {
}
// Say that we want to be a source of custom events.
YAHOO.lang.augmentProto(user_selector, YAHOO.util.EventProvider);
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;
YAHOO.util.Event.addListener('userselector_preserveselected', "change",
function(e) { oself.handle_option_change('userselector_preserveselected') });
YAHOO.util.Event.addListener('userselector_autoselectunique', "change",
function(e) { oself.handle_option_change('userselector_autoselectunique') });
YAHOO.util.Event.addListener('userselector_searchanywhere', "change",
function(e) { oself.handle_option_change('userselector_searchanywhere') });
}
user_selector_options_tracker.prototype.handle_option_change = function(option) {
set_user_preference(option, document.getElementById(option).checked);
}