Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.443
diff -u -r1.443 common.inc
--- includes/common.inc	21 May 2005 18:33:59 -0000	1.443
+++ includes/common.inc	24 May 2005 00:12:47 -0000
@@ -1257,6 +1257,41 @@
 }
 
 /**
+ * Format a single-line text field that has uses Ajax for autocomplete.
+ *
+ * @param $title
+ *   The label for the text field.
+ * @param $name
+ *   The internal name used to refer to the field.
+ * @param $value
+ *   The initial value for the field at page load time.
+ * @param $size
+ *   A measure of the visible size of the field (passed directly to HTML).
+ * @param $maxlength
+ *   The maximum number of characters that may be entered in the field.
+ * @param $callback_path
+ *   A drupal path for the Ajax autocomplete callback.
+ * @param $description
+ *   Explanatory text to display after the form item.
+ * @param $attributes
+ *   An associative array of HTML attributes to add to the form item.
+ * @param $required
+ *   Whether the user must enter some text in the field.
+ * @return
+ *   A themed HTML string representing the field.
+ */
+function form_autocomplete($title, $name, $value, $size, $maxlength, $callback_path, $description = NULL, $attributes = NULL, $required = FALSE) {
+  drupal_add_js('misc/autocomplete.js');
+
+  $size = $size ? ' size="'. $size .'"' : '';
+
+  $output = theme('form_element', $title, '<input type="text" maxlength="'. $maxlength .'" class="'. _form_get_class('form-text form-autocomplete', $required, _form_get_error($name)) .'" name="edit['. $name .']" id="edit-'. $name .'"'. $size .' value="'. check_plain($value) .'"'. drupal_attributes($attributes) .' />', $description, 'edit-'. $name, $required, _form_get_error($name));
+  $output .= '<input class="autocomplete" type="hidden" id="edit-'. $name .'-autocomplete" value="'. check_url(url($callback_path, NULL, NULL, TRUE)) .'" disabled="disabled" />';
+
+  return $output;
+}
+
+/**
  * Format a single-line text field that does not display its contents visibly.
  *
  * @param $title
@@ -1917,6 +1952,24 @@
   ');
 }
 
+/*
+* Add a JavaScript file to the output.
+*
+* The first time this function is invoked per page request,
+* it adds "misc/drupal.js" to the output. Other scripts
+* depends on the 'killswitch' inside it.
+*/
+function drupal_add_js($file) {
+  static $sent = array();
+  if (!$sent['misc/drupal.js']) {
+    drupal_set_html_head('<script type="text/javascript" src="misc/drupal.js"></script>');
+  }
+  if (!$sent[$file]) {
+    drupal_set_html_head('<script type="text/javascript" src="'. check_url($file) .'"></script>');
+    $sent[$file] = true;
+  }
+}
+
 // Set the Drupal custom error handler.
 set_error_handler('error_handler');
 
cvs diff: Diffing misc
Index: misc/autocomplete.js
===================================================================
RCS file: misc/autocomplete.js
diff -N misc/autocomplete.js
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ misc/autocomplete.js	24 May 2005 00:12:48 -0000
@@ -0,0 +1,255 @@
+// Global Killswitch
+if (isJSEnabled()) {
+  addLoadEvent(autocomplete_auto_attach);
+}
+
+/**
+ * Attaches the autocomplete behaviour to all required fields
+ */
+function autocomplete_auto_attach() {
+  var acdb = [];
+  var inputs = document.getElementsByTagName('input');
+  for (i = 0; input = inputs[i]; i++) {
+    if (input && hasClass(input, 'autocomplete')) {
+      uri = input.value;
+      if (!acdb[uri]) {
+        acdb[uri] = new ACDB(uri);
+      }
+      id = input.id.substr(0, input.id.length - 13);
+      alert(id);
+      input = document.getElementById(id);
+      input.setAttribute('autocomplete', 'OFF');
+      input.form.onsubmit = autocomplete_submit;
+      new jsAC(input, acdb[uri]);
+    }
+  }
+}
+
+/**
+ * Prevents the form from submitting if the suggestions popup is open
+ */
+function autocomplete_submit() {
+  var popup = document.getElementById('autocomplete');
+  if (popup) {
+    popup.owner.hidePopup();
+    return false;
+  }
+  return true;
+}
+
+
+/**
+ * An AutoComplete object
+ */
+function jsAC(input, db) {
+  var ac = this;
+  this.input = input;
+  this.db = db;
+  this.db.owner = this;
+  this.input.onkeydown = function (event) { return ac.onkeydown(this, event); };
+  this.input.onkeyup = function (event) { ac.onkeyup(this, event) };
+  this.input.onblur = function () { ac.hidePopup() };
+  this.popup = document.createElement('div');
+  this.popup.id = 'autocomplete';
+  this.popup.owner = this;
+};
+
+/**
+ * Hides the autocomplete suggestions
+ */
+jsAC.prototype.hidePopup = function (keycode) {
+  if (this.selected && ((keycode && keycode != 46 && keycode != 8 && keycode != 27) || !keycode)) {
+    this.input.value = this.selected.innerHTML;
+  }
+  if (this.popup.parentNode && this.popup.parentNode.tagName) {
+    removeNode(this.popup);
+  }
+  this.selected = false;
+}
+
+
+/**
+ * Handler for the "keydown" event
+ */
+jsAC.prototype.onkeydown = function (input, e) {
+  if (!e) {
+    e = window.event;
+  }
+  switch (e.keyCode) {
+    case 40: // down arrow
+      this.selectDown();
+      return false;
+    case 38: // up arrow
+      this.selectUp();
+      return false;
+    default: // all other keys
+      return true;
+  }
+}
+
+/**
+ * Handler for the "keyup" event
+ */
+jsAC.prototype.onkeyup = function (input, e) {
+  if (!e) {
+    e = window.event;
+  }
+  switch (e.keyCode) {
+    case 16: // shift
+    case 17: // ctrl
+    case 18: // alt
+    case 20: // caps lock
+    case 33: // page up
+    case 34: // page down
+    case 35: // end
+    case 36: // home
+    case 37: // left arrow
+    case 38: // up arrow
+    case 39: // right arrow
+    case 40: // down arrow
+      return true;
+
+    case 9:  // tab
+    case 13: // enter
+    case 27: // esc
+      this.hidePopup(e.keyCode);
+      return true;
+
+    default: // all other keys
+      if (input.value.length > 0)
+        this.populatePopup();
+      else
+        this.hidePopup(e.keyCode);
+      return true;
+  }
+}
+
+/**
+ * Puts the currently highlighted suggestion into the autocomplete field
+ */
+jsAC.prototype.select = function (node) {
+  this.input.value = node.innerHTML;
+}
+
+/**
+ * Highlights the next suggestion
+ */
+jsAC.prototype.selectDown = function () {
+  if (this.selected && this.selected.nextSibling) {
+    this.highlight(this.selected.nextSibling);
+  }
+  else {
+    var lis = this.popup.getElementsByTagName('li');
+    if (lis.length > 0) {
+      this.highlight(lis[0]);
+    }
+  }
+}
+
+/**
+ * Highlights the previous suggestion
+ */
+jsAC.prototype.selectUp = function () {
+  if (this.selected && this.selected.previousSibling) {
+    this.highlight(this.selected.previousSibling);
+  }
+}
+
+/**
+ * Highlights a suggestion
+ */
+jsAC.prototype.highlight = function (node) {
+  removeClass(this.selected, 'selected');
+  addClass(node, 'selected');
+  this.selected = node;
+}
+
+/**
+ * Unhighlights a suggestion
+ */
+jsAC.prototype.unhighlight = function (node) {
+  removeClass(node, 'selected');
+  this.selected = false;
+}
+
+/**
+ * Positions the suggestions popup and starts a search
+ */
+jsAC.prototype.populatePopup = function () {
+  var ac = this;
+  var pos = AbsolutePosition(this.input);
+  this.selected = false;
+  this.popup.style.top   = (pos.y + this.input.offsetHeight) +'px';
+  this.popup.style.left  = pos.x +'px';
+  this.popup.style.width = (this.input.offsetWidth - 4) +'px';
+  addClass(this.input, 'throbbing');
+  this.db.search(this.input.value);
+}
+
+/**
+ * Fills the suggestion popup with any matches received
+ */
+jsAC.prototype.found = function (matches) {
+  while (this.popup.hasChildNodes()) {
+    this.popup.removeChild(this.popup.childNodes[0]);
+  }
+  if (!this.popup.parentNode || !this.popup.parentNode.tagName) {
+    document.getElementsByTagName('body')[0].appendChild(this.popup);
+  }
+  var ul = document.createElement('ul');
+  var ac = this;
+  if (matches.length > 0) {
+    for (i in matches) {
+      li = document.createElement('li');
+      li.appendChild(document.createTextNode(matches[i]));
+      li.onmousedown = function() { ac.select(this); };
+      li.onmouseover = function() { ac.highlight(this); };
+      li.onmouseout  = function() { ac.unhighlight(this); };
+      ul.appendChild(li);
+    }
+    this.popup.appendChild(ul);
+  }
+  else {
+    this.hidePopup();
+  }
+  removeClass(this.input, 'throbbing');
+}
+
+/**
+ * An AutoComplete DataBase object
+ */
+function ACDB(uri) {
+  this.uri = uri;
+  this.max = 15;
+  this.delay = 300;
+  this.cache = {};
+}
+
+/**
+ * Performs a cached and delayed search
+ */
+ACDB.prototype.search = function(search_string) {
+  if (this.docache) {
+    this.search_string = search_string;
+    if (this.cache[search_string]) {
+      return this.match(this.cache[search_string]);
+    }
+  }
+  if (this.timer) {
+    clearTimeout(this.timer);
+  }
+  var db = this;
+  this.timer = setTimeout(function() { HTTPGet(db.uri +'/'+ search_string +'/'+ db.max, db.receive, db); }, this.delay);
+}
+
+/**
+ * HTTP callback function. Passes suggestions to the autocomplete object
+ */
+ACDB.prototype.receive = function(string, xmlhttp, acdb) {
+  if (xmlhttp.status != 200) {
+    return alert('An HTTP error '+ xmlhttp.status +' occured.\n'+ acdb.uri);
+  }
+  var matches = string.length > 0 ? string.split('|') : [];
+  acdb.cache[acdb.search_string] = matches;
+  acdb.owner.found(matches);
+}
Index: misc/drupal.css
===================================================================
RCS file: /cvs/drupal/drupal/misc/drupal.css,v
retrieving revision 1.103
diff -u -r1.103 drupal.css
--- misc/drupal.css	21 May 2005 11:53:01 -0000	1.103
+++ misc/drupal.css	24 May 2005 00:12:48 -0000
@@ -551,3 +551,28 @@
 ul.secondary a.active {
   border-bottom: 4px solid #999;
 }
+
+/*
+** Autocomplete styles
+*/
+#autocomplete {
+  position: absolute;
+  border: 1px solid;
+  overflow: hidden;
+}
+#autocomplete ul {
+  margin: 0;
+  padding: 0;
+  list-style: none;
+}
+#autocomplete li {
+  background: #fff;
+  color: #000;
+  white-space: pre;
+  cursor: default;
+}
+#autocomplete li.selected {
+  background: #888;
+  color: #fff;
+}
+
Index: misc/drupal.js
===================================================================
RCS file: misc/drupal.js
diff -N misc/drupal.js
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ misc/drupal.js	24 May 2005 00:12:48 -0000
@@ -0,0 +1,163 @@
+/**
+ * Only enable Javascript functionality if all required features are supported.
+ */
+function isJSEnabled() {
+  if (document.jsEnabled == undefined) {
+    // Note: ! casts to boolean implicitly.
+    document.jsEnabled = !(
+     !document.getElementsByTagName ||
+     !document.createElement        ||
+     !document.createTextNode       ||
+     !document.getElementById);
+  }
+  return document.jsEnabled;
+}
+
+// Global Killswitch
if (isJSEnabled()) {
+
+}

+/**
+ * Make IE's XMLHTTP object accessible through XMLHttpRequest()
+ */
+if (typeof XMLHttpRequest == 'undefined') {
+ XMLHttpRequest = function () {
+   var msxmls = ['MSXML3', 'MSXML2', 'Microsoft']
+   for (var i=0; i < msxmls.length; i++) {
+     try {
+       return new ActiveXObject(msxmls[i]+'.XMLHTTP')
+     }
+     catch (e) { }
+   }
+   throw new Error("No XML component installed!")
+ }
+}
+
+/**
+ * Creates an HTTP request and sends the response to the callback function
+ */
+function HTTPGet(uri, callback_function, callback_parameter) {
+  var xmlhttp = new XMLHttpRequest();
+  var bAsync = true;
+  if (!callback_function)
+    bAsync = false;
+  xmlhttp.open('GET', uri, bAsync);
+  xmlhttp.send(null);
+
+  if (bAsync) {
+    if (callback_function) {
+      xmlhttp.onreadystatechange = function() {
+        if (xmlhttp.readyState == 4)
+          callback_function(xmlhttp.responseText, xmlhttp, callback_parameter)
+      }
+    }
+    return true;
+  }
+  else {
+    return xmlhttp.responseText;
+  }
+}
+
+/**
+ * Adds a function to the window onload event
+ */
+function addLoadEvent(func) {
+  var oldonload = window.onload;
+  if (typeof window.onload != 'function') {
+    window.onload = func;
+  }
+  else {
+    window.onload = function() {
+      oldonload();
+      func();
+    }
+  }
+}
+
+/**
+ * Retrieves the absolute position of an element on the screen
+ */
+function AbsolutePosition(el) {
+  var SL = 0, ST = 0;
+  var is_div = /^div$/i.test(el.tagName);
+  if (is_div && el.scrollLeft) {
+    SL = el.scrollLeft;
+  }
+  if (is_div && el.scrollTop) {
+    ST = el.scrollTop;
+  }
+  var r = { x: el.offsetLeft - SL, y: el.offsetTop - ST };
+  if (el.offsetParent) {
+    var tmp = AbsolutePosition(el.offsetParent);
+    r.x += tmp.x;
+    r.y += tmp.y;
+  }
+  return r;
+};
+
+/**
+ * Returns true if an element has a specified class name
+ */
+function hasClass(node, className) {
+  if (node.className == className) {
+    return true;
+  }
+  var reg = new RegExp('(^| )'+ className +'($| )')
+  if (reg.test(node.className)) {
+    return true;
+  }
+  return false;
+}
+
+/**
+ * Adds a class name to an element
+ */
+function addClass(node, className) {
+  if (hasClass(node, className)) {
+    return false;
+  }
+  node.className += ' '+ className;
+  return true;
+}
+
+/**
+ * Removes a class name from an element
+ */
+function removeClass(node, className) {
+  if (!hasClass(node, className)) {
+    return false;
+  }
+  node.className = ereg_replace('(^| )'+ className +'($| )', '', node.className);
+  return true;
+}
+
+/**
+ * Toggles a class name on or off for an element
+ */
+function toggleClass(node, className) {
+  if (!removeClass(node, className) && !addClass(node, className)) {
+    return false;
+  }
+  return true;
+}
+
+/**
+ * Emulate PHP's ereg_replace function in javascript
+ */
+function ereg_replace(search, replace, subject) {
+  return subject.replace(new RegExp(search,'g'), replace);
+}
+
+/**
+ * Removes an element from the page
+ */
+function removeNode(node) {
+  if (typeof node == 'string') {
+    node = document.getElementById(node);
+  }
+  if (node && node.parentNode) {
+    return node.parentNode.removeChild(node);
+  }
+  else {
+    return false;
+  }
+}
cvs diff: Diffing modules
Index: modules/node.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/node.module,v
retrieving revision 1.488
diff -u -r1.488 node.module
--- modules/node.module	17 May 2005 20:24:33 -0000	1.488
+++ modules/node.module	24 May 2005 00:12:51 -0000
@@ -1317,7 +1317,7 @@
   if (user_access('administer nodes')) {
     $output .= '<div class="admin">';
 
-    $author = form_textfield(t('Authored by'), 'name', $edit->name, 20, 60);
+    $author = form_autocomplete(t('Authored by'), 'name', $edit->name, 20, 60, 'user/autocomplete');
     $author .= form_textfield(t('Authored on'), 'date', $edit->date, 20, 25, NULL, NULL, TRUE);
 
     $output .= '<div class="authored">';
Index: modules/user.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/user.module,v
retrieving revision 1.473
diff -u -r1.473 user.module
--- modules/user.module	21 May 2005 11:57:59 -0000	1.473
+++ modules/user.module	24 May 2005 00:12:57 -0000
@@ -638,6 +638,9 @@
     $items[] = array('path' => 'user', 'title' => t('user account'),
       'callback' => 'user_page', 'access' => TRUE, 'type' => MENU_CALLBACK);
 
+    $items[] = array('path' => 'user/autocomplete', 'title' => t('user autocomplete'),
+      'callback' => 'user_autocomplete', 'access' => $admin_access, 'type' => MENU_CALLBACK);
+
     //registration and login pages.
     $items[] = array('path' => 'user/login', 'title' => t('log in'),
       'type' => MENU_DEFAULT_LOCAL_TASK);
@@ -1851,4 +1854,17 @@
   return $output;
 }
 
+/**
+ * Retrieve a pipe delimited string of autocomplete suggestions for existing users
+ */
+function user_autocomplete($string) {
+  $matches = array();
+  $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;
+  }
+  print check_plain(implode('|', $matches));
+  exit();
+}
+
 ?>
