? balloon
? select.html
? misc/theme.js
? modules/user/access_control.js
? sites/all/modules
? sites/all/modules_repository
Index: includes/form.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/form.inc,v
retrieving revision 1.194
diff -u -d -F^\s*function -r1.194 form.inc
--- includes/form.inc	7 May 2007 10:15:57 -0000	1.194
+++ includes/form.inc	12 May 2007 12:09:03 -0000
@@ -1421,13 +1421,12 @@ function theme_token($element) {
 function theme_textfield($element) {
   $size = $element['#size'] ? ' size="'. $element['#size'] .'"' : '';
   $class = array('form-text');
-  $extra = '';
   $output = '';
 
   if ($element['#autocomplete_path']) {
     drupal_add_js('misc/autocomplete.js');
+    drupal_add_js(array('autocomplete' => array($element['#id'] => url($element['#autocomplete_path']))), 'setting');
     $class[] = 'form-autocomplete';
-    $extra =  '<input class="autocomplete" type="hidden" id="'. $element['#id'] .'-autocomplete" value="'. check_url(url($element['#autocomplete_path'], array('absolute' => TRUE))) .'" disabled="disabled" />';
   }
   _form_set_class($element, $class);
 
@@ -1441,7 +1440,7 @@ function theme_textfield($element) {
     $output .= ' <span class="field-suffix">'. $element['#field_suffix'] .'</span>';
   }
 
-  return theme('form_element', $element, $output) . $extra;
+  return theme('form_element', $element, $output);
 }
 
 /**
Index: misc/autocomplete.js
===================================================================
RCS file: /cvs/drupal/drupal/misc/autocomplete.js,v
retrieving revision 1.17
diff -u -d -F^\s*function -r1.17 autocomplete.js
--- misc/autocomplete.js	9 Jan 2007 07:31:04 -0000	1.17
+++ misc/autocomplete.js	12 May 2007 12:09:03 -0000
@@ -1,54 +1,63 @@
 // $Id: autocomplete.js,v 1.17 2007/01/09 07:31:04 drumm Exp $
 
-/**
- * Attaches the autocomplete behaviour to all required fields
- */
-Drupal.autocompleteAutoAttach = function () {
-  var acdb = [];
-  $('input.autocomplete').each(function () {
-    var uri = this.value;
-    if (!acdb[uri]) {
-      acdb[uri] = new Drupal.ACDB(uri);
-    }
-    var input = $('#' + this.id.substr(0, this.id.length - 13))
-      .attr('autocomplete', 'OFF')[0];
-    $(input.form).submit(Drupal.autocompleteSubmit);
-    new Drupal.jsAC(input, acdb[uri]);
-  });
-}
+Drupal.autocomplete = { handlers: {} };
 
 /**
  * Prevents the form from submitting if the suggestions popup is open
  * and closes the suggestions popup when doing so.
  */
-Drupal.autocompleteSubmit = function () {
+Drupal.autocomplete.submit = function () {
   return $('#autocomplete').each(function () {
     this.owner.hidePopup();
   }).size() == 0;
 }
 
 /**
+ * Attaches the autocomplete behaviour to all required fields
+ */
+Drupal.autocomplete.attach = function () {
+  if (!Drupal.settings || !Drupal.settings.autocomplete) return;
+
+  var handlers = Drupal.autocomplete.handlers;
+  for (id in Drupal.settings.autocomplete) {
+    var url = Drupal.settings.autocomplete[id];
+    if (!handlers[url]) {
+      handlers[url] = new Drupal.autocomplete.handler(url);
+    }
+
+    new Drupal.autocomplete.field($('#' + id)[0], handlers[url]);
+  }
+}
+
+/**
  * An AutoComplete object
  */
-Drupal.jsAC = function (input, db) {
+Drupal.autocomplete.field = function (input, db) {
   var ac = this;
   this.input = input;
   this.db = db;
 
+  $(this.input.form).submit(Drupal.autocomplete.submit);
+
   $(this.input)
+    .attr('autocomplete', 'OFF')
     .keydown(function (event) { return ac.onkeydown(this, event); })
     .keyup(function (event) { ac.onkeyup(this, event) })
-    .blur(function () { ac.hidePopup(); ac.db.cancel(); });
+    .blur(function () {
+      var selected = ac.selected;
+      ac.hidePopup();
+      ac.db.cancel();
 
+      if (selected) {
+        ac.select();
+      }
+    });
 };
 
 /**
  * Handler for the "keydown" event
  */
-Drupal.jsAC.prototype.onkeydown = function (input, e) {
-  if (!e) {
-    e = window.event;
-  }
+Drupal.autocomplete.field.prototype.onkeydown = function (input, e) {
   switch (e.keyCode) {
     case 40: // down arrow
       this.selectDown();
@@ -64,10 +73,7 @@
 /**
  * Handler for the "keyup" event
  */
-Drupal.jsAC.prototype.onkeyup = function (input, e) {
-  if (!e) {
-    e = window.event;
-  }
+Drupal.autocomplete.field.prototype.onkeyup = function (input, e) {
   switch (e.keyCode) {
     case 16: // shift
     case 17: // ctrl
@@ -101,14 +107,22 @@
 /**
  * Puts the currently highlighted suggestion into the autocomplete field
  */
-Drupal.jsAC.prototype.select = function (node) {
-  this.input.value = node.autocompleteValue;
+Drupal.autocomplete.field.prototype.select = function (node) {
+  if (node) {
+    this.input.value = node.autocompleteValue;
+  }
+
+  if (this.input.selectionStart) {
+    this.input.select();
+    this.input.focus();
+    this.input.selectionStart = this.input.value.length;
+  }
 }
 
 /**
  * Highlights the next suggestion
  */
-Drupal.jsAC.prototype.selectDown = function () {
+Drupal.autocomplete.field.prototype.selectDown = function () {
   if (this.selected && this.selected.nextSibling) {
     this.highlight(this.selected.nextSibling);
   }
@@ -123,7 +137,7 @@
 /**
  * Highlights the previous suggestion
  */
-Drupal.jsAC.prototype.selectUp = function () {
+Drupal.autocomplete.field.prototype.selectUp = function () {
   if (this.selected && this.selected.previousSibling) {
     this.highlight(this.selected.previousSibling);
   }
@@ -132,7 +146,7 @@
 /**
  * Highlights a suggestion
  */
-Drupal.jsAC.prototype.highlight = function (node) {
+Drupal.autocomplete.field.prototype.highlight = function (node) {
   if (this.selected) {
     $(this.selected).removeClass('selected');
   }
@@ -143,7 +157,7 @@
 /**
  * Unhighlights a suggestion
  */
-Drupal.jsAC.prototype.unhighlight = function (node) {
+Drupal.autocomplete.field.prototype.unhighlight = function (node) {
   $(node).removeClass('selected');
   this.selected = false;
 }
@@ -151,10 +165,10 @@
 /**
  * Hides the autocomplete suggestions
  */
-Drupal.jsAC.prototype.hidePopup = function (keycode) {
+Drupal.autocomplete.field.prototype.hidePopup = function (keycode) {
   // Select item if the right key or mousebutton was pressed
   if (this.selected && ((keycode && keycode != 46 && keycode != 8 && keycode != 27) || !keycode)) {
-    this.input.value = this.selected.autocompleteValue;
+    this.select(this.selected);
   }
   // Hide popup
   var popup = this.popup;
@@ -168,20 +182,18 @@
 /**
  * Positions the suggestions popup and starts a search
  */
-Drupal.jsAC.prototype.populatePopup = function () {
+Drupal.autocomplete.field.prototype.populatePopup = function () {
   // Show popup
   if (this.popup) {
     $(this.popup).remove();
   }
   this.selected = false;
-  this.popup = document.createElement('div');
-  this.popup.id = 'autocomplete';
-  this.popup.owner = this;
-  $(this.popup).css({
+  this.popup = $('<div id="autocomplete"></div>').css({
     marginTop: this.input.offsetHeight +'px',
     width: (this.input.offsetWidth - 4) +'px',
     display: 'none'
   });
+  this.popup[0].owner = this;
   $(this.input).before(this.popup);
 
   // Do search
@@ -192,39 +204,43 @@
 /**
  * Fills the suggestion popup with any matches received
  */
-Drupal.jsAC.prototype.found = function (matches) {
+Drupal.autocomplete.field.prototype.found = function (matches) {
   // If no value in the textfield, do not show the popup.
   if (!this.input.value.length) {
     return false;
   }
 
   // Prepare matches
-  var ul = document.createElement('ul');
-  var ac = this;
-  for (key in matches) {
-    var li = document.createElement('li');
-    $(li)
-      .html('<div>'+ matches[key] +'</div>')
-      .mousedown(function () { ac.select(this); })
-      .mouseover(function () { ac.highlight(this); })
-      .mouseout(function () { ac.unhighlight(this); });
-    li.autocompleteValue = key;
-    $(ul).append(li);
+  var ul = $('<ul></ul>');
+  var field = this;
+  for (key in matches.matches) {
+    var li = $('<li class="clear-block"></li>')
+      .html(matches.matches[key])
+      .mouseover(function () { field.highlight(this); })
+      .mouseout(function () { field.unhighlight(this); })
+      .click(function() { field.select(this); return false; });
+    li[0].autocompleteValue = key;
+    ul.append(li);
   }
 
   // Show popup with matches, if any
   if (this.popup) {
-    if (ul.childNodes.length > 0) {
-      $(this.popup).empty().append(ul).show();
+    this.popup.empty();
+    if ($(ul).children().size()) {
+      this.popup.append(ul).show();
     }
-    else {
-      $(this.popup).css({visibility: 'hidden'});
+    if (matches.info) {
+      this.popup.append($('<div class="autocomplete-info"></div>').html(matches.info)).show();
+    }
+
+    if (!$(this.popup).children().size()) {
+      this.popup.css({visibility: 'hidden'});
       this.hidePopup();
     }
   }
 }
 
-Drupal.jsAC.prototype.setStatus = function (status) {
+Drupal.autocomplete.field.prototype.setStatus = function (status) {
   switch (status) {
     case 'begin':
       $(this.input).addClass('throbbing');
@@ -238,9 +254,9 @@
 }
 
 /**
- * An AutoComplete DataBase object
+ * The default autocomplete handler with AJAX callback
  */
-Drupal.ACDB = function (uri) {
+Drupal.autocomplete.handler = function (uri) {
   this.uri = uri;
   this.delay = 300;
   this.cache = {};
@@ -249,7 +265,7 @@
 /**
  * Performs a cached and delayed search
  */
-Drupal.ACDB.prototype.search = function (searchString) {
+Drupal.autocomplete.handler.prototype.search = function (searchString) {
   var db = this;
   this.searchString = searchString;
 
@@ -291,7 +307,7 @@
 /**
  * Cancels the current autocomplete request
  */
-Drupal.ACDB.prototype.cancel = function() {
+Drupal.autocomplete.handler.prototype.cancel = function() {
   if (this.owner) this.owner.setStatus('cancel');
   if (this.timer) clearTimeout(this.timer);
   this.searchString = '';
@@ -299,5 +315,5 @@
 
 // Global Killswitch
 if (Drupal.jsEnabled) {
-  $(document).ready(Drupal.autocompleteAutoAttach);
+  $(Drupal.autocomplete.attach);
 }
Index: modules/profile/profile.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/profile/profile.module,v
retrieving revision 1.200
diff -u -d -F^\s*function -r1.200 profile.module
--- modules/profile/profile.module	30 Apr 2007 17:03:27 -0000	1.200
+++ modules/profile/profile.module	12 May 2007 12:09:03 -0000
@@ -730,16 +730,17 @@ function profile_form_profile($edit, $us
  * Callback to allow autocomplete of profile text fields.
  */
 function profile_autocomplete($field, $string) {
+  $matches = array();
   if (db_result(db_query("SELECT COUNT(*) FROM {profile_fields} WHERE fid = %d AND autocomplete = 1", $field))) {
-    $matches = array();
     $result = db_query_range("SELECT value FROM {profile_values} WHERE fid = %d AND LOWER(value) LIKE LOWER('%s%%') GROUP BY value ORDER BY value ASC", $field, $string, 0, 10);
     while ($data = db_fetch_object($result)) {
       $matches[$data->value] = check_plain($data->value);
     }
-
-    print drupal_to_js($matches);
   }
-  exit();
+
+  // Set the correct content type for JSON output.
+  drupal_set_header('Content-Type: text/javascript; charset=utf-8');
+  echo drupal_to_js(array('matches' => $matches));
 }
 
 /**
Index: modules/system/system.css
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.css,v
retrieving revision 1.26
diff -u -d -F^\s*function -r1.26 system.css
--- modules/system/system.css	10 May 2007 19:55:23 -0000	1.26
+++ modules/system/system.css	12 May 2007 12:09:04 -0000
@@ -279,6 +279,10 @@
   border: 1px solid;
   overflow: hidden;
   z-index: 100;
+  background: #fff;
+  color: #000;
+  cursor: default;
+  line-height: normal;
 }
 #autocomplete ul {
   margin: 0;
@@ -286,15 +290,24 @@
   list-style: none;
 }
 #autocomplete li {
-  background: #fff;
-  color: #000;
-  white-space: pre;
+  padding: 0.25em 0;
+  clear: both;
   cursor: default;
 }
 #autocomplete li.selected {
   background: #0072b9;
   color: #fff;
 }
+#autocomplete li small {
+  font-size: 0.85em;
+  color: #555;
+}
+#autocomplete li.selected * {
+  color:#fff;
+}
+#autocomplete .autocomplete-info {
+  padding:0.25em 0.5em;
+}
 /* Animated throbber */
 html.js input.form-autocomplete {
   background-image: url(../../misc/throbber.gif);
Index: modules/taxonomy/taxonomy.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/taxonomy/taxonomy.module,v
retrieving revision 1.354
diff -u -d -F^\s*function -r1.354 taxonomy.module
--- modules/taxonomy/taxonomy.module	4 May 2007 08:38:34 -0000	1.354
+++ modules/taxonomy/taxonomy.module	12 May 2007 12:09:04 -0000
@@ -21,6 +21,9 @@ function taxonomy_theme() {
     'taxonomy_term_select' => array(
       'arguments' => array('element' => NULL),
     ),
+    'taxonomy_autocomplete_item' => array(
+      'arguments' => array('title', 'description'),
+    ),
   );
 }
 
@@ -1495,25 +1498,61 @@ function taxonomy_autocomplete($vid, $st
   // The user enters a comma-separated list of tags. We only autocomplete the last tag.
   $array = taxonomy_explode_tags($string);
 
+  $matches = array();
+  $info = '';
+
   // Fetch last tag
   $last_string = trim(array_pop($array));
-  if ($last_string != '') {
-    $result = db_query_range(db_rewrite_sql("SELECT t.tid, t.name FROM {term_data} t WHERE t.vid = %d AND LOWER(t.name) LIKE LOWER('%%%s%%')", 't', 'tid'), $vid, $last_string, 0, 10);
+  if (!empty($last_string)) {
+    $result = db_query(db_rewrite_sql("SELECT t.tid, t.name, t.description FROM {term_data} t WHERE t.vid = %d AND LOWER(t.name) LIKE LOWER('%%%s%%')", 't', 'tid'), $vid, $last_string);
 
-    $prefix = count($array) ? implode(', ', $array) .', ' : '';
+    // The rest of the autocomplete string (i.e. not modified tags).
+    $prefix = count($array) ? taxonomy_implode_tags($array) .', ' : '';
 
-    $matches = array();
+    // Loop through the result set.
     while ($tag = db_fetch_object($result)) {
-      $n = $tag->name;
-      // Commas and quotes in terms are special cases, so encode 'em.
-      if (strpos($tag->name, ',') !== FALSE || strpos($tag->name, '"') !== FALSE) {
-        $n = '"'. str_replace('"', '""', $tag->name) .'"';
+      // Check if the tag is not yet in the list of already entered tags.
+      if (!in_array($tag->name, $array)) {
+        $id = $tag->name;
+        // Commas and quotes in terms are special cases, so encode them.
+        if (strpos($id, ',') !== FALSE || strpos($id, '"') !== FALSE) {
+          $n = '"'. str_replace('"', '""', $id) .'"';
+        }
+
+        // Highlight the search string in the result name.
+        $title = preg_replace('%('. $last_string .')%i', '<strong>$1</strong>', check_plain($tag->name));
+
+        // Add the tag to the list of tags.
+        $matches[$prefix . $id .', '] = theme('taxonomy_autocomplete_item', $title, check_plain($tag->description));
       }
-      $matches[$prefix . $n] = check_plain($tag->name);
     }
-    print drupal_to_js($matches);
-    exit();
+
+    // Add info text on certain occasions.
+    $total = db_num_rows($result);
+    if (count($matches) == 0) {
+      $info = t('There are no matching terms.');
+    }
+    elseif ($total > 10) {
+      $info = format_plural($total - 10, 'There is one further matching term.', 'There are @count further matching terms.');
+    }
   }
+
+  // Set the correct content type for JSON output.
+  drupal_set_header('Content-Type: text/javascript; charset=utf-8');
+  echo drupal_to_js(array('matches' => $matches, 'info' => $info));
+}
+
+/**
+ * Themes a taxonomy term list item for an autocomplete dropdown.
+ *
+ * @ingroup themeable
+ */
+function theme_taxonomy_autocomplete_item($title, $description = '') {
+  $output = '<span class="term-name">'. $title .'</span>';
+  if (!empty($description)) {
+    $output .= ' <small class="term-description">'. $description .'</small>';
+  }
+  return $output;
 }
 
 /**
@@ -1542,19 +1581,29 @@ function taxonomy_explode_tags($tags) {
 
 /**
  * Implode a list of tags of a certain vocabulary into a string.
+ *
+ * @param $tags
+ *   An array of taxonomy term objects or an array of taxonomy term strings.
+ * @param $vid
+ *   (Optional). If specified, only taxonomy terms with this vocabulary id are
+ *   imploded together. Terms not belonging to this vocabulary are simply omitted.
  */
 function taxonomy_implode_tags($tags, $vid = NULL) {
   $typed_tags = array();
   foreach ($tags as $tag) {
     // Extract terms belonging to the vocabulary in question.
-    if (is_null($vid) || $tag->vid == $vid) {
+    if (!isset($vid) || (isset($tag->vid) && $tag->vid == $vid)) {
+      // If we have a list of term objects, flatten the list to only the name.
+      if (is_object($tag)) {
+        $tag = $tag->name;
+      }
 
       // Commas and quotes in tag names are special cases, so encode 'em.
-      if (strpos($tag->name, ',') !== FALSE || strpos($tag->name, '"') !== FALSE) {
-        $tag->name = '"'. str_replace('"', '""', $tag->name) .'"';
+      if (strpos($tag, ',') !== FALSE || strpos($tag, '"') !== FALSE) {
+        $tag = '"'. str_replace('"', '""', $tag) .'"';
       }
 
-      $typed_tags[] = $tag->name;
+      $typed_tags[] = $tag;
     }
   }
   return implode(', ', $typed_tags);
Index: modules/user/user.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/user/user.module,v
retrieving revision 1.779
diff -u -d -F^\s*function -r1.779 user.module
--- modules/user/user.module	10 May 2007 19:55:23 -0000	1.779
+++ modules/user/user.module	12 May 2007 12:09:06 -0000
@@ -56,6 +56,9 @@ function user_theme() {
     'user_signature' => array(
       'arguments' => array('signature' => NULL),
     ),
+    'user_autocomplete_item' => array(
+      'arguments' => array('account' => NULL),
+    ),
   );
 }
 
@@ -727,6 +730,21 @@ function theme_user_list($users, $title 
   return theme('item_list', $items, $title);
 }
 
+/**
+ * Themes a user autocomplete list item for display in the dropdown.
+ *
+ * @param $account
+ *   A user object containing only the user id and the name of the user.
+ * @param $search
+ *   The search term the user entered.
+ *
+ * @ingroup themeable
+ */
+function theme_user_autocomplete_item($account, $search) {
+  // Highlight the search string in the result name.
+  return preg_replace('%('. $search .')%i', '<strong>$1</strong>', check_plain($account->name));
+}
+
 function user_is_anonymous() {
   return !$GLOBALS['user']->uid;
 }
@@ -2609,14 +2627,29 @@ function _user_forms(&$edit, $account, $
  */
 function user_autocomplete($string = '') {
   $matches = array();
-  if ($string) {
-    $result = db_query_range("SELECT name FROM {users} WHERE LOWER(name) LIKE LOWER('%s%%')", $string, 0, 10);
-    while ($user = db_fetch_object($result)) {
-      $matches[$user->name] = check_plain($user->name);
-   }
+  $info = '';
+
+  if (!empty($string)) {
+    $result = db_query("SELECT uid, name FROM {users} WHERE LOWER(name) LIKE LOWER('%%%s%%')", $string);
+
+    $i = 0;
+    while ($i < 10 && $user = db_fetch_object($result)) {
+      $matches[$user->name] = theme('user_autocomplete_item', $user, $string);
+      $i++;
+    }
+
+    $total = db_num_rows($result);
+    if (count($matches) == 0) {
+      $info = t('No matching users were found.');
+    }
+    elseif ($total > 10) {
+      $info = format_plural($total - 10, 'The ten best matches are listed. One further user has been found.', 'The ten best matches are listed. @count further users have been found.');
+    }
   }
-  print drupal_to_js($matches);
-  exit();
+
+  // Set the correct content type for JSON output.
+  drupal_set_header('Content-Type: text/javascript; charset=utf-8');
+  echo drupal_to_js(array('matches' => $matches, 'info' => $info));
 }
 
 /**
Index: themes/garland/style.css
===================================================================
RCS file: /cvs/drupal/drupal/themes/garland/style.css,v
retrieving revision 1.19
diff -u -d -F^\s*function -r1.19 style.css
--- themes/garland/style.css	13 Apr 2007 07:33:24 -0000	1.19
+++ themes/garland/style.css	12 May 2007 12:09:07 -0000
@@ -806,8 +806,9 @@
  */
 #autocomplete li {
   cursor: default;
-  padding: 2px;
+  padding: 0.25em;
   margin: 0;
+  background: none;
 }
 
 /**
