]> git.mjollnir.org Git - moodle.git/commitdiff
user selection: MDL-17073 add options to control the search in a collapsible region.
authortjhunt <tjhunt>
Tue, 4 Nov 2008 05:12:12 +0000 (05:12 +0000)
committertjhunt <tjhunt>
Tue, 4 Nov 2008 05:12:12 +0000 (05:12 +0000)
lang/en_utf8/moodle.php
lib/javascript-static.js
lib/weblib.php
theme/standard/styles_layout.css
user/selector/lib.php
user/selector/script.js

index 935b117855488c1a1725a50178598e47c4ff8f1d..7fa071581cb24e10e879fbce7cb92e0a6a1a6f99 100644 (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';
index 3b9f0840c5d744c5e74efb1b01f6c98e49620c84..30cf3ada05a4fada51b8de0cd8bf9539c4728af5 100644 (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';
+    }
+}
\ No newline at end of file
index eff30575ff507e5d975a1455a628bfcec0b965d9..edc7b2fdea019f6fbe651713215433d5d4a93149 100644 (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;
index 6660fa97ec79453d00ea73649ae2f6b7c4cb21f2..39d0e8f45693bd4b028e07d017fd0b629e27e638 100644 (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,
index 8b52e21cf196e6c8e46b511832b54fb8772d5296..f74e120c20cbfffa353e272716da4d49162c3952 100644 (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;
     }
 }
index e238b17b9f5cedba63870ba7fdca1bc8cc00b122..aed9acd5f3d8a903453a01cb688ccd56f8b73853 100644 (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);
\ 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