Index: modules/taxonomy/taxonomy.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/taxonomy/taxonomy.module,v
retrieving revision 1.493
diff -u -p -r1.493 taxonomy.module
--- modules/taxonomy/taxonomy.module	4 Aug 2009 06:50:07 -0000	1.493
+++ modules/taxonomy/taxonomy.module	12 Aug 2009 04:07:36 -0000
@@ -84,6 +84,9 @@ function taxonomy_theme() {
     'field_formatter_taxonomy_term_plain' => array(
       'arguments' => array('element' => NULL),
     ),
+    'taxonomy_autocomplete' => array(
+      'arguments' => array('element' => NULL),
+    ),
   );
 }
 
@@ -1777,6 +1780,37 @@ function taxonomy_implode_tags($tags, $v
 }
 
 /**
+ * Implement hook_field_settings_form().
+ */
+function taxonomy_field_settings_form($field, $instance) {
+  // Get the right values for allowed_values_function, which is a core setting.
+  $options = array();
+  $vocabularies = taxonomy_get_vocabularies();
+  foreach ($vocabularies as $vocabulary) {
+    $options[$vocabulary->vid] = $vocabulary->name;
+  }
+  $form['allowed_values'] = array(
+    '#tree' => TRUE,
+  );
+  foreach ($field['settings']['allowed_values'] as $delta => $tree) {
+    $form['allowed_values'][$delta]['vid'] = array(
+      '#type' => 'select',
+      '#title' => t('Vocabulary'),
+      '#default_value' => $tree['vid'],
+      '#options' => $options,
+      '#required' => TRUE,
+      '#description' => t('The vocabulary which supplies the options for this field.'),
+    );
+    $form['allowed_values'][$delta]['parent'] = array(
+      '#type' => 'value',
+      '#value' => $tree['parent'],
+    );
+  }
+
+  return $form;
+}
+
+/**
  * Implement hook_hook_info().
  */
 function taxonomy_hook_info() {
@@ -1828,6 +1862,35 @@ function taxonomy_field_info() {
 }
 
 /**
+ * Implement hook_field_widget_info().
+ *
+ * We need custom handling of multiple values because we need
+ * to combine them into a options list rather than display
+ * cardinality elements. We will use the field module's default
+ * handling for default values.
+ *
+ * Callbacks can be omitted if default handing is used.
+ * They're included here just so this module can be used
+ * as an example for custom modules that might do things
+ * differently.
+ */
+function taxonomy_field_widget_info() {
+  return array(
+    'taxonomy_autocomplete' => array(
+      'label' => t('Autocomplete term widget (tagging)'),
+      'field types' => array('taxonomy_term'),
+      'settings' => array(
+        'size' => 60,
+        'autocomplete_path' => 'taxonomy/autocomplete',
+      ),
+      'behaviors' => array(
+        'multiple values' => FIELD_BEHAVIOR_CUSTOM,
+      ),
+    ),
+  );
+}
+
+/**
  * Implement hook_field_widget_info_alter().
  */
 function taxonomy_field_widget_info_alter(&$info) {
@@ -2039,3 +2102,161 @@ function _taxonomy_clean_field_cache($te
 function taxonomy_term_title($term) {
   return check_plain($term->name);
 }
+
+/**
+ * Implement hook_field_widget().
+ */
+function taxonomy_field_widget(&$form, &$form_state, $field, $instance, $items, $delta = NULL) {
+  $element = array(
+    '#type' => $instance['widget']['type'],
+    '#default_value' => !empty($items) ? $items : array(),
+  );
+  return $element;
+}
+
+/**
+ * Implement hook_field_widget_error().
+ */
+function taxonomy_field_widget_error($element, $error) {
+  $field_key  = $element['#columns'][0];
+  form_error($element[$field_key], $error['message']);
+}
+
+/**
+ * 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 taxonomy_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' => 'taxonomy/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'], 'taxonomy_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 taxonomy term autocomplete element.
+ */
+function taxonomy_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']) {
+    // @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) {
+        $vocabulary = taxonomy_vocabulary_load($vids[0]);
+        // 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;
+      //}
+    }
+    $results = options_transpose_array_rows_cols(array($field_key => $values));
+    $terms = taxonomy_term_load_multiple(array_keys($results));
+    foreach ($results as $tid => &$result) {
+      $result['taxonomy_term'] = $terms[$tid];
+    }
+    form_set_value($element, $results, $form_state);
+  }
+}
+
+/**
+ * Implement FAPI hook_elements().
+ *
+ * Any FAPI callbacks needed for individual widgets can be declared here,
+ * and the element will be passed to those callbacks for processing.
+ *
+ * Drupal will automatically theme the element using a theme with
+ * the same name as the hook_elements key.
+ */
+function taxonomy_elements() {
+  return array(
+    'taxonomy_autocomplete' => array(
+      '#input' => TRUE,
+      '#columns' => array('value'),
+      '#delta' => 0,
+      '#process' => array('taxonomy_autocomplete_elements_process'),
+    ),
+  );
+}
Index: modules/taxonomy/taxonomy.pages.inc
===================================================================
RCS file: /cvs/drupal/drupal/modules/taxonomy/taxonomy.pages.inc,v
retrieving revision 1.31
diff -u -p -r1.31 taxonomy.pages.inc
--- modules/taxonomy/taxonomy.pages.inc	8 Jul 2009 07:18:08 -0000	1.31
+++ modules/taxonomy/taxonomy.pages.inc	12 Aug 2009 04:07:36 -0000
@@ -90,27 +90,43 @@ function taxonomy_term_edit($term) {
 /**
  * Helper function for autocompletion
  */
-function taxonomy_autocomplete($vid = 0, $tags_typed = '') {
+function taxonomy_autocomplete($field_name, $bundle, $tags_typed = '') {
+  $instance = field_info_instance($field_name, $bundle);
+  $field = field_info_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 != '') {
+
+    // 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');
-    $query->leftJoin('taxonomy_term_synonym', 'ts', 't.tid = ts.tid');
+    // 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', $vid)
+      ->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 . '%'))
-	    ->where("LOWER(ts.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()
@@ -119,7 +135,9 @@ function taxonomy_autocomplete($vid = 0,
     $prefix = count($tags_typed) ? implode(', ', $tags_typed) . ', ' : '';
 
     // We use two arrays to make sure synonym suggestions appear last.
-    $term_matches = $synonym_matches = array();
+    // 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.
@@ -127,15 +145,16 @@ function taxonomy_autocomplete($vid = 0,
         $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);
-      }
+      //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(array_merge($term_matches, $synonym_matches));
+  drupal_json($term_matches);
 }
