Index: includes/common.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/common.inc,v retrieving revision 1.617 diff -u -d -F^\s*function -r1.617 common.inc --- includes/common.inc 15 Feb 2007 11:40:17 -0000 1.617 +++ includes/common.inc 6 Mar 2007 14:45:01 -0000 @@ -1753,6 +1753,24 @@ function drupal_to_js($var) { } /** + * Outputs a properly structured autocomplete object and sets the right header. + * + * @param $matches + * An indexed array with the items found for the keyword. The value of the + * textfield will be replaced with the array's key when an item is selected. + * The value is shown to the user. + * @param $info + * (Optional) If set, the info text will be displayed together with the + * items, but will not be selectable. + */ +function drupal_autocomplete($matches, $info = '') { + // 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' => theme('autocomplete_info', $info))); +} + +/** * Wrapper around urlencode() which avoids Apache quirks. * * Should be used when placing arbitrary data in an URL. Note that Drupal paths Index: includes/form.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/form.inc,v retrieving revision 1.179 diff -u -d -F^\s*function -r1.179 form.inc --- includes/form.inc 27 Feb 2007 12:45:13 -0000 1.179 +++ includes/form.inc 6 Mar 2007 14:45:04 -0000 @@ -876,6 +876,14 @@ function _form_set_value(&$form_values, } /** + * Handles autocomplete functionality for an element. + */ +function form_autocomplete($element) { + drupal_add_js('misc/autocomplete.js'); + drupal_add_js(array('autocomplete' => array($element['#id'] => url($element['#autocomplete_path']))), 'setting'); +} + +/** * Retrieve the default properties for the defined element type. */ function _element_info($type, $refresh = NULL) { @@ -1411,13 +1419,11 @@ 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'); + form_autocomplete($element); $class[] = 'form-autocomplete'; - $extra = ''; } _form_set_class($element, $class); @@ -1431,7 +1437,7 @@ function theme_textfield($element) { $output .= ' '. $element['#field_suffix'] .''; } - return theme('form_element', $element, $output). $extra; + return theme('form_element', $element, $output); } /** Index: includes/theme.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/theme.inc,v retrieving revision 1.343 diff -u -d -F^\s*function -r1.343 theme.inc --- includes/theme.inc 2 Mar 2007 09:40:13 -0000 1.343 +++ includes/theme.inc 6 Mar 2007 14:45:06 -0000 @@ -1122,6 +1122,22 @@ function theme_progress_bar($percent, $m return $output; } +function theme_autocomplete_item($primary, $secondary = NULL, $description = NULL) { + $output .= '
'. $primary .'
'; + if (!empty($secondary)) { + $output .= '
'. $secondary .'
'; + } + if (!empty($description)) { + $output .= ''. $description .''; + } + return $output; +} + +function theme_autocomplete_info($info) { + if (!empty($info)) { + return '
'. $info .'
'; + } +} /** * @} End of "defgroup themeable". */ 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 6 Mar 2007 14:45:07 -0000 @@ -1,51 +1,55 @@ // $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(); }); - }; /** * Handler for the "keydown" event */ -Drupal.jsAC.prototype.onkeydown = function (input, e) { +Drupal.autocomplete.field.prototype.onkeydown = function (input, e) { if (!e) { e = window.event; } @@ -64,7 +68,7 @@ /** * Handler for the "keyup" event */ -Drupal.jsAC.prototype.onkeyup = function (input, e) { +Drupal.autocomplete.field.prototype.onkeyup = function (input, e) { if (!e) { e = window.event; } @@ -101,14 +105,17 @@ /** * Puts the currently highlighted suggestion into the autocomplete field */ -Drupal.jsAC.prototype.select = function (node) { +Drupal.autocomplete.field.prototype.select = function (node) { this.input.value = node.autocompleteValue; + if (this.input.selectionStart) { + 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 +130,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 +139,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 +150,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 +158,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 +175,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 = $('
').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 +197,42 @@ /** * 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('
'+ matches[key] +'
') - .mousedown(function () { ac.select(this); }) - .mouseover(function () { ac.highlight(this); }) - .mouseout(function () { ac.unhighlight(this); }); - li.autocompleteValue = key; - $(ul).append(li); + var ul = $(''); + var field = this; + for (key in matches.matches) { + var li = $('
  • ') + .html(matches.matches[key]) + .mouseover(function () { field.highlight(this); }) + .mouseout(function () { field.unhighlight(this); }); + 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($('
    ').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'); @@ -240,7 +248,7 @@ /** * An AutoComplete DataBase object */ -Drupal.ACDB = function (uri) { +Drupal.autocomplete.handler = function (uri) { this.uri = uri; this.delay = 300; this.cache = {}; @@ -249,7 +257,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 +299,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 +307,5 @@ // Global Killswitch if (Drupal.jsEnabled) { - $(document).ready(Drupal.autocompleteAutoAttach); + $(document).ready(Drupal.autocomplete.attach); } Index: modules/profile/profile.module =================================================================== RCS file: /cvs/drupal/drupal/modules/profile/profile.module,v retrieving revision 1.194 diff -u -d -F^\s*function -r1.194 profile.module --- modules/profile/profile.module 11 Feb 2007 09:30:51 -0000 1.194 +++ modules/profile/profile.module 6 Mar 2007 14:45:10 -0000 @@ -705,16 +705,14 @@ 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); + $matches[$data->value] = theme('autocomplete_item', check_plain($data->value)); } - - print drupal_to_js($matches); } - exit(); + drupal_autocomplete($matches); } /** Index: modules/system/system.css =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.css,v retrieving revision 1.23 diff -u -d -F^\s*function -r1.23 system.css --- modules/system/system.css 2 Mar 2007 09:40:13 -0000 1.23 +++ modules/system/system.css 6 Mar 2007 14:45:10 -0000 @@ -273,6 +273,10 @@ border: 1px solid; overflow: hidden; z-index: 100; + background: #fff; + color: #000; + cursor: default; + line-height: normal; } #autocomplete ul { margin: 0; @@ -280,15 +284,32 @@ list-style: none; } #autocomplete li { - background: #fff; - color: #000; - white-space: pre; - cursor: default; + padding: 0.25em 0; + clear: both; } #autocomplete li.selected { background: #0072b9; color: #fff; } +#autocomplete li .primary { + font-weight: bold; + float: left; +} +#autocomplete li .secondary { + float: right; +} +#autocomplete li small { + font-size: 0.85em; + color: #555; + display: block; + clear: both; +} +#autocomplete li.selected * { + color:#fff; +} +#autocomplete div.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.341 diff -u -d -F^\s*function -r1.341 taxonomy.module --- modules/taxonomy/taxonomy.module 27 Feb 2007 12:48:33 -0000 1.341 +++ modules/taxonomy/taxonomy.module 6 Mar 2007 14:45:14 -0000 @@ -1475,24 +1475,35 @@ function taxonomy_autocomplete($vid, $st $regexp = '%(?:^|,\ *)("(?>[^"]*)(?>""[^"]* )*"|(?: [^",]*))%x'; preg_match_all($regexp, $string, $matches); $array = $matches[1]; + $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); + $result = pager_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'), 10, 'taxonomy_autocomplete', NULL, $vid, $last_string); $prefix = count($array) ? implode(', ', $array) .', ' : ''; - $matches = array(); 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) .'"'; + 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) .'"'; + } + $matches[$prefix . $id .', '] = theme('autocomplete_item', check_plain($tag->name), l(t('List'), 'taxonomy/term/'. $tag->tid), check_plain($tag->description)); } - $matches[$prefix . $n] = check_plain($tag->name); } - print drupal_to_js($matches); - exit(); } + + global $pager_total_items; + if (count($matches) == 0) { + $info = t('There are no matches.'); + } + elseif (isset($pager_total_items) && $pager_total_items['taxonomy_autocomplete'] > 10) { + $info = format_plural($pager_total_items['taxonomy_autocomplete'] - 10, 'There is one further match.', 'There are @count further matches.'); + } + + drupal_autocomplete($matches, $info); } Index: modules/user/user.module =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.module,v retrieving revision 1.756 diff -u -d -F^\s*function -r1.756 user.module --- modules/user/user.module 15 Feb 2007 11:40:18 -0000 1.756 +++ modules/user/user.module 6 Mar 2007 14:45:20 -0000 @@ -684,6 +684,13 @@ function theme_user_list($users, $title return theme('item_list', $items, $title); } +function theme_user_autocomplete_item($user) { + $output = '
    '. check_plain($user->name) .'
    '; + $output .= '
    '. l('('. t('View profile') .')', 'user/'. $user->uid) .'
    '; + + return $output; +} + function user_is_anonymous() { return !$GLOBALS['user']->uid; } @@ -2541,14 +2548,24 @@ function _user_forms(&$edit, $account, $ */ function user_autocomplete($string = '') { $matches = array(); + $info = ''; if ($string) { - $result = db_query_range("SELECT name FROM {users} WHERE LOWER(name) LIKE LOWER('%s%%')", $string, 0, 10); + $result = pager_query("SELECT uid FROM {users} WHERE LOWER(name) LIKE LOWER('%s%%')", 10, 'user_autocomplete', NULL, $string); while ($user = db_fetch_object($result)) { - $matches[$user->name] = check_plain($user->name); - } + $user = user_load($user->uid); + $matches[$user->name] = theme('user_autocomplete_item', $user); + } } - print drupal_to_js($matches); - exit(); + + global $pager_total_items; + if (count($matches) == 0) { + $info = t('No matching users were found.'); + } + elseif (isset($pager_total_items) && $pager_total_items['user_autocomplete'] > 10) { + $info = format_plural($pager_total_items['user_autocomplete'] - 10, 'The ten best matches are listed. One further user was found.', 'The ten best matches are listed. @count further users were found.'); + } + + drupal_autocomplete($matches, $info); } /** Index: themes/garland/style.css =================================================================== RCS file: /cvs/drupal/drupal/themes/garland/style.css,v retrieving revision 1.17 diff -u -d -F^\s*function -r1.17 style.css --- themes/garland/style.css 2 Mar 2007 09:40:14 -0000 1.17 +++ themes/garland/style.css 6 Mar 2007 14:45:22 -0000 @@ -804,8 +804,9 @@ */ #autocomplete li { cursor: default; - padding: 2px; + padding: 0.25em; margin: 0; + background: none; } /**