From 904998d8f9ce45856a356565089cbd3854bed346 Mon Sep 17 00:00:00 2001 From: tjhunt Date: Tue, 4 Nov 2008 05:12:12 +0000 Subject: [PATCH] user selection: MDL-17073 add options to control the search in a collapsible region. --- lang/en_utf8/moodle.php | 4 ++ lib/javascript-static.js | 16 ++++-- lib/weblib.php | 2 +- theme/standard/styles_layout.css | 13 ++++- user/selector/lib.php | 87 ++++++++++++++++++++++++++++---- user/selector/script.js | 69 ++++++++++++++++++++----- 6 files changed, 162 insertions(+), 29 deletions(-) diff --git a/lang/en_utf8/moodle.php b/lang/en_utf8/moodle.php index 935b117855..7fa071581c 100644 --- a/lang/en_utf8/moodle.php +++ b/lang/en_utf8/moodle.php @@ -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.

word : find any match of this word within the text.
+word : only exact matching words will be found.
-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'; diff --git a/lib/javascript-static.js b/lib/javascript-static.js index 3b9f0840c5..30cf3ada05 100644 --- a/lib/javascript-static.js +++ b/lib/javascript-static.js @@ -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'; + } +} \ No newline at end of file diff --git a/lib/weblib.php b/lib/weblib.php index eff30575ff..edc7b2fdea 100644 --- a/lib/weblib.php +++ b/lib/weblib.php @@ -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 = ''; + $output = ''; if ($return) { return $output; diff --git a/theme/standard/styles_layout.css b/theme/standard/styles_layout.css index 6660fa97ec..39d0e8f456 100644 --- a/theme/standard/styles_layout.css +++ b/theme/standard/styles_layout.css @@ -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, diff --git a/user/selector/lib.php b/user/selector/lib.php index 8b52e21cf1..f74e120c20 100644 --- a/user/selector/lib.php +++ b/user/selector/lib.php @@ -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 .= ''; + + // 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 .= "\n\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 = '

' . + ' ' . + '

\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; } } diff --git a/user/selector/script.js b/user/selector/script.js index e238b17b9f..aed9acd5f3 100644 --- a/user/selector/script.js +++ b/user/selector/script.js @@ -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. + */ +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); \ No newline at end of file +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); +} \ No newline at end of file -- 2.39.5