Index: modules/field/modules/options/options.info
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/modules/options/options.info,v
retrieving revision 1.3
diff -u -p -r1.3 options.info
--- modules/field/modules/options/options.info	12 Jun 2009 08:39:36 -0000	1.3
+++ modules/field/modules/options/options.info	9 Aug 2009 16:29:06 -0000
@@ -1,8 +1,9 @@
 ; $Id: options.info,v 1.3 2009/06/12 08:39:36 dries Exp $
 name = Options
 description = Defines selection, check box and radio button widgets for text and numeric fields.
 package = Core - fields
 version = VERSION
 core = 7.x
 files[]=options.module
+files[]=options.pages.inc
 required = TRUE
Index: modules/field/modules/options/options.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/field/modules/options/options.module,v
retrieving revision 1.8
diff -u -p -r1.8 options.module
--- modules/field/modules/options/options.module	1 Aug 2009 06:03:12 -0000	1.8
+++ modules/field/modules/options/options.module	9 Aug 2009 16:29:06 -0000
@@ -7,6 +7,19 @@
  */
 
 /**
+ * Implement hook_menu().
+ */
+function options_menu() {
+  $items['options/autocomplete'] = array(
+    'title' => 'Autocomplete options',
+    'page callback' => 'options_autocomplete',
+    'access arguments' => array('access content'),
+    'type' => MENU_CALLBACK,
+  );
+  return $items;
+}
+
+/**
  * Implement hook_theme().
  */
 function options_theme() {
@@ -23,6 +36,9 @@ function options_theme() {
     'options_none' => array(
       'arguments' => array('widget_type' => NULL, 'field_name' => NULL, 'node_type' => NULL),
       ),
+    'options_autocomplete' => array(
+      'arguments' => array('element' => NULL),
+    ),
   );
 }
 
@@ -63,6 +79,14 @@ function options_field_widget_info() {
         'multiple values' => FIELD_BEHAVIOR_CUSTOM,
       ),
     ),
+    'options_autocomplete' => array(
+      'label' => t('Autocomplete text field'),
+      'field types' => array('list', 'list_boolean', 'list_text', 'list_number'),
+      'settings' => array('size' => 60, 'autocomplete_path' => 'options/autocomplete'),
+      'behaviors' => array(
+        'multiple values' => FIELD_BEHAVIOR_CUSTOM,
+      ),
+    ),
   );
 }
 
@@ -92,6 +116,11 @@ function options_elements() {
       '#columns' => array('value'), '#delta' => 0,
       '#process' => array('options_onoff_elements_process'),
       ),
+    'options_autocomplete' => array(
+      '#input' => TRUE,
+      '#columns' => array('value'), '#delta' => 0,
+      '#process' => array('options_autocomplete_elements_process'),
+    ),
     );
 }
 
@@ -103,6 +132,7 @@ function options_field_widget(&$form, &$
     '#type' => $instance['widget']['type'],
     '#default_value' => !empty($items) ? $items : array(),
   );
+  // TODO: autocomplete textfield needs to do something here.
   return $element;
 }
 
@@ -255,6 +285,131 @@ function options_onoff_elements_process(
 }
 
 /**
+ * Process an individual element.
+ *
+ * Build the form element. When creating a form using FAPI #process,
+ * note that $element['#value'] is already set.
+ *
+ * The $field and $instance arrays are in $form['#fields'][$element['#field_name']].
+ *
+ * TODO: For widgets to be actual FAPI 'elements', reusable outside of a
+ * 'field' context, they shoudn't rely on $field and $instance. The bits of
+ * information needed to adjust the behavior of the 'element' should be
+ * extracted in hook_field_widget() above.
+ */
+function options_autocomplete_elements_process($element, &$form_state, $form) {
+  $field = $form['#fields'][$element['#field_name']]['field'];
+  $instance = $form['#fields'][$element['#field_name']]['instance'];
+  $field_key = $element['#columns'][0];
+
+  // See if this element is in the database format or the transformed format,
+  // and transform it if necessary.
+  if (is_array($element['#value']) && !array_key_exists($field_key, $element['#value'])) {
+    $tags = array();
+    foreach ($element['#default_value'] as $item) {
+      // TODO: do i need to load the term? when is the term not available?
+      $tags[$item['value']] = isset($item['taxonomy_term']) ? $item['taxonomy_term'] : taxonomy_term_load($item['value']);
+    }
+    $element['#value'] = taxonomy_implode_tags($tags);
+  }
+  $typed_string = $element['#value'];
+  //$options = options_options($field, $instance);
+  //$multiple = isset($element['#multiple']) ? $element['#multiple'] : $field['cardinality'] > 1 || $field['cardinality'] == FIELD_CARDINALITY_UNLIMITED;
+
+  $value = array();
+  $element[$field_key] = array(
+    '#type' => 'textfield',
+    '#default_value' => $typed_string,
+    //'#autocomplete_path' => !empty($instance['widget']['settings']['autocomplete_path']) ? $instance['widget']['settings']['autocomplete_path'] : NULL,
+    '#autocomplete_path' => 'options/autocomplete/'. $element['#field_name'] .'/'. $element['#bundle'],
+    '#size' => $instance['widget']['settings']['size'],
+    '#attributes' => array('class' => 'text'),
+    '#title' => $element['#title'],
+    '#description' => $element['#description'],
+    '#required' => $element['#required'],
+  );
+  $element[$field_key]['#maxlength'] = !empty($field['settings']['max_length']) ? $field['settings']['max_length'] : NULL;
+
+// Set #element_validate in a way that it will not wipe out other
+  // validation functions already set by other modules.
+  if (empty($element['#element_validate'])) {
+    $element['#element_validate'] = array();
+  }
+  array_unshift($element['#element_validate'], 'options_autocomplete_validate');
+
+  // Make sure field info will be available to the validator which does not get
+  // the values in $form.
+  $form_state['#fields'][$element['#field_name']] = $form['#fields'][$element['#field_name']];
+  return $element;
+}
+
+/**
+ * FAPI function to validate options autocomplete element.
+ */
+function options_autocomplete_validate($element, &$form_state) {
+  // Free tagging vocabularies do not send their tids in the form,
+  // so we'll detect them here and process them independently.
+  if ($form_state['values'][$element['#field_name']]['value']) {
+
+    /**
+     * @todo Use the same module or API hook required for autocomplete, which
+     * must also support multiple values so validation, transformation can be
+     * quick.
+     * @see options_autocomplete
+     */
+
+    // @see taxonomy_node_save
+    $field = $form_state['#fields'][$element['#field_name']]['field'];
+    $field_key = $element['#columns'][0];
+    $vids = array();
+    foreach ($field['settings']['allowed_values'] as $tree) {
+      $vids[] = $tree['vid'];
+    }
+    // TODO: make this configurable.
+    $typed_terms = drupal_explode_tags($form_state['values'][$element['#field_name']]['value']);
+    // TODO: come to a better understanding of what duplicate values mean for
+    // this widget. user input == 'Red, red, "RED", rEd'
+    //$inserted = array();
+    $values = array();
+    foreach ($typed_terms as $typed_term) {
+      // See if the term exists in the chosen vocabulary
+      // and return the tid; otherwise, add a new record.
+      $possibilities = taxonomy_term_load_multiple(array(), array('name' => trim($typed_term), 'vid' => $vids));
+      $typed_term_tid = NULL; // tid match, if any.
+      foreach ($possibilities as $possibility) {
+        $typed_term_tid = $possibility->tid;
+        // TODO: we need to handle multiple possibilities in a better way.
+        break;
+      }
+      if (!$typed_term_tid) {
+        dpm('new term needs to be added: '. $typed_term);
+        $vocabulary = taxonomy_vocabulary_load($vids[0]);
+        // TODO: add a new term to the vocabulary.
+        // TODO: determine whether the vocabulary/subtree to add beneath is
+        // a field setting or widget setting and whether it's an instance
+        // setting.
+        $edit = array(
+          'vid' => $vids[0],
+          'name' => $typed_term,
+          'vocabulary_machine_name' => $vocabulary->machine_name,
+        );
+        $term = (object)$edit;
+        $status = taxonomy_term_save($term);
+        $typed_term_tid = $term->tid;
+      }
+      $values[$typed_term_tid] = $typed_term_tid;
+
+      // Defend against duplicate, differently cased tags
+      //if (!isset($inserted[$typed_term_tid])) {
+      //  $inserted[$typed_term_tid] = TRUE;
+      //}
+    }
+    $result = options_transpose_array_rows_cols(array($field_key => $values));
+    form_set_value($element, $result, $form_state);
+  }
+}
+
+/**
  * FAPI function to validate options element.
  */
 function options_validate($element, &$form_state) {
Index: modules/field/modules/options/options.pages.inc
===================================================================
RCS file: modules/field/modules/options/options.pages.inc
diff -N modules/field/modules/options/options.pages.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/field/modules/options/options.pages.inc	9 Aug 2009 16:29:06 -0000
@@ -0,0 +1,88 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Page callbacks for the options module.
+ */
+
+/**
+ * Helper function for autocompletion
+ */
+function options_autocomplete($field_name, $bundle, $tags_typed = '') {
+  $instance = field_read_instance($field_name, $bundle);
+  $field = field_read_field($field_name);
+
+  // The user enters a comma-separated list of tags. We only autocomplete the last tag.
+  $tags_typed = drupal_explode_tags($tags_typed);
+  $tag_last = drupal_strtolower(array_pop($tags_typed));
+
+  $matches = array();
+  if ($tag_last != '') {
+
+    /**
+     * @todo Describe a module or API hook that fields implement to support a
+     * kind of searching specific to the field type.
+     *   - taxonomy term fields: taxonomy_autocomplete.
+     *   - list fields: a function that searches allowed values array.
+     *   - text fields: field_attach_query.
+     * @see options_autocomplete_validate
+     */
+
+    // Part of the criteria for the query come from the field's own settings.
+    $vids = array();
+    foreach ($field['settings']['allowed_values'] as $tree) {
+      $vids[] = $tree['vid'];
+    }
+    // TODO: Support subtrees. Can't be done in the query but can be done in
+    // a validation step.
+    // TODO: Document the costs of using subtrees vs. whole vocabularies in
+    // settings on autocomplete widgets for taxonomy term fields.
+
+    $query = db_select('taxonomy_term_data', 't');
+    $query->addTag('term_access');
+    // TODO: Support synonyms... but how?
+    //$query->leftJoin('taxonomy_term_synonym', 'ts', 't.tid = ts.tid');
+    // Don't select already entered terms.
+    if (count($tags_typed)) {
+      $query->condition('t.name', $tags_typed, 'NOT IN');
+    }
+    $tags_return = $query
+      ->fields('t', array('tid', 'name'))
+      ->condition('t.vid', $vids, 'IN')
+      // Select rows that either match by term or synonym name.
+      ->condition(db_or()
+	    ->where("LOWER(t.name) LIKE :last_string", array(':last_string' => '%' . $tag_last . '%'))
+      // TODO: Support synonyms... but how?
+	    //->where("LOWER(ts.name) LIKE :last_string", array(':last_string' => '%' . $tag_last . '%'))
+      )
+      ->range(0, 10)
+      ->execute()
+      ->fetchAllKeyed();
+
+    $prefix = count($tags_typed) ? implode(', ', $tags_typed) . ', ' : '';
+
+    // We use two arrays to make sure synonym suggestions appear last.
+    // TODO: Support synonyms... but how?
+    //$term_matches = $synonym_matches = array();
+    $term_matches = array();
+    foreach ($tags_return as $tid => $name) {
+      $n = $name;
+      // Commas and quotes in terms are special cases, so encode 'em.
+      if (strpos($name, ',') !== FALSE || strpos($name, '"') !== FALSE) {
+        $n = '"' . str_replace('"', '""', $name) . '"';
+      }
+      // Inform the user his query matched a synonym rather than a term.
+      //if (strpos(drupal_strtolower($name), $tag_last) === FALSE) {
+      //  $name = t('Did you mean %suggestion', array('%suggestion' => $name));
+      //  $synonym_matches[$prefix . $n] = filter_xss($name);
+      //}
+      else {
+        $term_matches[$prefix . $n] = filter_xss($name);
+      }
+    }
+  }
+
+  //drupal_json(array_merge($term_matches, $synonym_matches));
+  drupal_json($term_matches);
+}
