Drupal: Multiple Autocomplete Forms

Sat, Jan 10, 2009 - 1:20pm -- Isaac Sukin

I've been working on a module for BabelUp lately called user_deco. It allows users to add images ("decos") to the site, and then "buy" them using a point system (userpoints). The decos are then added to the user's profile. It's a great way to encourage community participation, because decos are a fun way to "spend" userpoints (known as BabelPoints on BabelUp) and userpoints are gained for contributing content on the site.

I got hung up as I was developing a way to send decos to other users, however. I wanted a form that would automatically look for users as the current user was typing, known as autocompletion. That's built into Drupal core (Drupal is a system on which the site was built) but only allows one user at a time to be entered into the field. I needed users to be able to send decos to multiple users at a time, separated by commas.

I knew that the privatemsg module did this, so I went and looked at the code there. It needed a lot of tweaking, but this is what I came up with. It works on any generic Drupal form (Drupal 5 and Drupal 6 versions are below).

/**
 * Implementation of hook_menu().
 */
function user_deco_menu($may_cache) {
  $items = array();
  if (!$may_cache) {
    $items[] = array(
      'path' => 'user_deco/autocomplete',
      'title' => t('User Deco user autocomplete'),
      'callback' => 'user_deco_user_autocomplete',
      'access' => user_access('send user_decos'),
      'type' => MENU_CALLBACK,
    );
    //Put another page here where you want the form to show up.
  }
  return $items;
}

/**
 * Form to send decos to other users.
 */
function user_deco_send() {
  $form['name'] = array(
    '#type' => 'textfield',
    '#title' => t('Send to'),
    '#maxlength' => 60,
    '#autocomplete_path' => 'user_deco/autocomplete',
  );
  $form['submit'] = array('#type' => 'submit', '#value' => t('Submit'));
  return $form;
}

//Put functions here to do something with the result of user_deco_send.

/**
 * Drupal 5 version.  Adapted from the Privatemsg module.
 * Some awkwardness exists if users have commas in their names.
 *
 * @param $string
 *   The list of names.
 */
function user_deco_user_autocomplete($string) {
  $names = explode(',', $string);
  for ($i = 0; $i < count($names); $i++) {
    $names[$i] = trim($names[$i]);
  }
  $search = array_pop($names);

  if ($search != '') {
    $result = db_query_range("SELECT name FROM {users} WHERE status <> 0 AND LOWER(name) LIKE LOWER('%s%%') ORDER BY name ASC", $search, 0, 10);
    $prefix = '';
    if (count($names)) {
      $prefix = implode(', ', $names) .', ';
    }
    $matches = array();
    while ($user = db_fetch_object($result)) {
      $matches[$prefix . $user->name] = check_plain($user->name);
    }
    print drupal_to_js($matches);
    exit();
  }
}

/**
 * Drupal 6 version.  Adapted from taxonomy.module.
 * Some awkwardness exists if a user has quotes or commas in their username.
 *
 * @param $string
 *   The list of names.
 */
function user_deco_user_autocomplete($string = '') {
  $array = drupal_explode_tags($string);
  //The user enters a comma-separated list of names. We only autocomplete the last name.
  $search = trim(array_pop($array));
  $matches = array();
  if ($search != '') {
    $result = db_query_range("SELECT DISTINCT(name) FROM (SELECT u.name FROM {users} u LEFT JOIN {users_roles} r ON u.uid = r.uid
      WHERE u.status <> 0 AND u.uid <> 0 AND LOWER(u.name) LIKE LOWER('%s%%')
        AND (r.rid IN (SELECT rid FROM {permission} WHERE perm LIKE '%%buy user_decos%%') OR (SELECT rid FROM {permission} WHERE rid = 2 AND perm LIKE '%%buy user_decos%%'))
      ORDER BY u.name ASC) x", $search, 0, 10);
    $prefix = count($array) ? implode(', ', $array) .', ' : '';
    while ($user = db_fetch_object($result)) {
      $name = $user->name;
      //Commas and quotes in terms are special cases, so encode them.  Use strpos() to check if they exist first because str_replace() is expensive.
      if (strpos($user->name, ',') !== FALSE || strpos($user->name, '"') !== FALSE) {
        $name = '"'. str_replace('"', '""', $user->name) .'"';
      }
      $matches[$prefix . $name] = check_plain($user->name);
    }
  }
  drupal_json($matches);
}