? nr_autocomplete.install
Index: nr_autocomplete.info
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/nr_autocomplete/nr_autocomplete.info,v
retrieving revision 1.1
diff -u -p -r1.1 nr_autocomplete.info
--- nr_autocomplete.info	7 Jan 2008 03:43:15 -0000	1.1
+++ nr_autocomplete.info	15 Mar 2010 15:22:11 -0000
@@ -1,5 +1,6 @@
 ; $Id: nr_autocomplete.info,v 1.1 2008/01/07 03:43:15 joshbenner Exp $
 name=Nodereference Autocomplete Widget
 description=Adds an autocomplete widget for nodereference that works like taxonomy free-tagging
-dependencies=content nodereference
-package=CCK
\ No newline at end of file
+dependencies[]=nodereference
+package=CCK
+core=6.x
Index: nr_autocomplete.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/nr_autocomplete/nr_autocomplete.module,v
retrieving revision 1.2
diff -u -p -r1.2 nr_autocomplete.module
--- nr_autocomplete.module	21 Nov 2008 12:56:01 -0000	1.2
+++ nr_autocomplete.module	15 Mar 2010 15:22:12 -0000
@@ -1,55 +1,32 @@
 <?php 
 // $Id: nr_autocomplete.module,v 1.2 2008/11/21 12:56:01 joshbenner Exp $
+
 /**
  * @file
  * Nodereference autocomplete widget module file
  */
 
 /**
- * Implementation of hook_menu()
+ * Implementation of hook_menu().
  */
-function nr_autocomplete_menu($may_cache) {
+function nr_autocomplete_menu() {
   $items = array();
-  if ($may_cache) {
-    $items[] = array(
-      'path' => 'nr_autocomplete/autocomplete',
-      'title' => t('nr_autocomplete autocomplete'),
-      'callback' => 'nr_autocomplete_autocomplete',
-      'access' => user_access('access content'),
-      'type' => MENU_CALLBACK
-    );
-  }
+  $items['nr_autocomplete/autocomplete'] = array(
+    'title' => 'Enhanced Nodereference autocomplete',
+    'page callback' => 'nr_autocomplete_autocomplete',
+    'access arguments' => array('access content'),
+    'type' => MENU_CALLBACK
+  );
   return $items;
 }
 
 /**
- * Retrieve a pipe delimited string of autocomplete suggestions
- * 
- * Unholy union of taxonomy.module's taxonomy_autocomplete() and
- * nodreference.module's nodreference_autocomplete().
+ * Implementation of hook_theme().
  */
-function nr_autocomplete_autocomplete($field_name, $string = '') {
-  $fields = content_fields();
-  $field = $fields[$field_name];
-
-  // Pattern from taxonomy_autocomplete()
-  $regexp = '%(?:^|,\ *)("(?>[^"]*)(?>""[^"]* )*"(?> [^",]*)|(?: [^",]*))%x';
-  preg_match_all($regexp, $string, $matches);
-  $array = $matches[1];
-
-  // Fetch last node title
-  $last_string = trim(array_pop($array));
-  
-  if ($last_string != '') {
-    $prefix = count($array) ? implode(', ', $array) .', ' : '';
-    $matches = array();
-    foreach (_nodereference_potential_references($field, TRUE, $last_string) as $row) {
-      $n = nr_autocomplete_encode($row->node_title);
-      $matches[$prefix . $n .' [nid:'. $row->nid .']'] = _nodereference_item($field, $row, TRUE);
-    }
-    print drupal_to_js($matches);
-  }
-  exit();
+function nr_autocomplete_theme() {
+  return array(
+    'nr_autocomplete' => array('arguments' => array('element' => NULL)),
+  );
 }
 
 /**
@@ -58,111 +35,241 @@ function nr_autocomplete_autocomplete($f
 function nr_autocomplete_widget_info() {
   return array(
     'nr_autocomplete' => array(
-      'label' => t('Enhanced Nodereference Autocomplete (Single Text Field for Multiple Values)'),
+      'label' => t('Enhanced Nodereference Autocomplete'),
       'field types' => array('nodereference'),
+      'multiple values' => CONTENT_HANDLE_MODULE,
+      'callbacks' => array(
+        'default value' => CONTENT_CALLBACK_DEFAULT,
+      ),
     ),
   );
 }
 
 /**
- * Implementation of hook_widget().
+ * Implementation of 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.
+ *
+ * Autocomplete_path is not used by text_widget but other widgets can use it
+ * (see nodereference and userreference).
  */
-function nr_autocomplete_widget($op, &$node, $field, &$items) {
-  switch ($op) {
-    case 'prepare form values':
-      $names = array();
-      foreach ($items as $delta => $item) {
-        if (!empty($item['nid'])) {
-          $n = db_result(db_query(db_rewrite_sql('SELECT n.title FROM {node} n WHERE n.nid = %d'), $item['nid']));
-          $items[$delta]['default node_name'] = nr_autocomplete_encode($n) . ' [nid:'. $item['nid'] .']';
-        }
-      }
-      break;
+function nr_autocomplete_elements() {
+  return array(
+    'nr_autocomplete' => array(
+      '#input' => TRUE,
+      '#columns' => array('name'),
+      '#delta' => 0,
+      '#process' => array('nr_autocomplete_process'),
+      '#autocomplete_path' => FALSE,
+    ),
+  );
+}
 
+/**
+ * Implementation of hook_widget_settings().
+ */
+function nr_autocomplete_widget_settings($op, $widget) {
+  switch ($op) {
     case 'form':
       $form = array();
-      $form[$field['field_name']] = array('#tree' => TRUE);
-
-      if ($field['multiple']) {
-        $delta = 0;
-        $names = array();
-        foreach ($items as $item) {
-          if ($item['nid']) {
-            $names[] = $item['default node_name'];
-          }
-        }
-        $form[$field['field_name']][0]['node_name'] = array(
-          '#type' => 'textfield',
-          '#title' => t($field['widget']['label']),
-          '#description' => t($field['widget']['description']),
-          '#autocomplete_path' => 'nr_autocomplete/autocomplete/'. $field['field_name'],
-          '#default_value' => implode(', ', $names),
-          '#required' => $field['required'],
+      $match = isset($widget['autocomplete_match']) ? $widget['autocomplete_match'] : 'contains';
+      if ($widget['type'] == 'nr_autocomplete') {
+        $form['autocomplete_match'] = array(
+          '#type' => 'select',
+          '#title' => t('Autocomplete matching'),
+          '#default_value' => $match,
+          '#options' => array(
+            'starts_with' => t('Starts with'),
+            'contains' => t('Contains'),
+          ),
+          '#description' => t('Select the method used to collect autocomplete suggestions. Note that <em>Contains</em> can cause performance issues on sites with thousands of nodes.'),
         );
       }
       else {
-        // Use nodereference's autocomplete in a single item scenario
-        $form[$field['field_name']][0]['node_name'] = array(
-          '#type' => 'textfield',
-          '#title' => t($field['widget']['label']),
-          '#autocomplete_path' => 'nodereference/autocomplete/'. $field['field_name'],
-          '#default_value' => $items[0]['default node_name'],
-          '#required' => $field['required'],
-          '#description' => t($field['widget']['description']),
-        );
+        $form['autocomplete_match'] = array('#type' => 'hidden', '#value' => $match);
       }
       return $form;
 
-    case 'validate':
-      $typed_titles = nr_autocomplete_parse_input($items[0]['node_name']);
-      foreach ($typed_titles as $delta => $item) {
-        $error_field = $field['field_name'] .']['. $delta .'][node_name';
-        if (!empty($item['node_name'])) {
-          preg_match('/^(?:\s*|(.*) )?\[\s*nid\s*:\s*(\d+)\s*\]$/', $item['node_name'], $matches);
-          if (!empty($matches)) {
-            // explicit nid
-            list(, $title, $nid) = $matches;
-            // Check for encoded and unencoded matches
-            if (!empty($title) && ($n = node_load($nid)) && nr_autocomplete_encode($title) != $n->title && $title != $n->title) {
-              form_set_error($error_field, t('%name : Title mismatch. Please check your selection.'), array('%name' => t($field['widget']['label'])));
-            }
-          }
-        }
-      }
-      return;
+    case 'save':
+      return array('autocomplete_match');
+  }
+}
 
-    case 'process form values':
-      $typed_titles = nr_autocomplete_parse_input($items[0]['node_name']);
-      // Remove the widget's data representation so it isn't saved.
-      unset($items[0]['node_name']);
-      foreach ($typed_titles as $delta => $title) {
-        $nid = 0;
-        if (!empty($title)) {
-          preg_match('/^(?:\s*|(.*) )?\[\s*nid\s*:\s*(\d+)\s*\]$/', $title, $matches);
-          if (!empty($matches)) {
-            // explicit nid
-            $nid = $matches[2];
-          }
-          else {
-            // no explicit nid
-            // TODO :
-            // the best thing would be to present the user with an additional form,
-            // allowing the user to choose between valid candidates with the same title
-            // ATM, we pick the first matching candidate...
-            $nids = _nodereference_potential_references($field, FALSE, $title, TRUE);
-            $nid = (!empty($nids)) ? array_shift(array_keys($nids)) : 0;
-          }
-        }
-        if (!empty($nid)) {
-          $items[$delta]['nid'] = $nid;
-          $items[$delta]['error_field'] = $field['field_name'] .'][0][node_name';
+/**
+ * Implementation of hook_widget().
+ */
+function nr_autocomplete_widget(&$form, &$form_state, $field, $items, $delta = 0) {
+  $element = array(
+    '#type' => $field['widget']['type'],
+    '#default_value' => $items,
+    '#value_callback' => 'nr_autocomplete_value',
+  );
+  return $element;
+}
+
+/**
+ * Value for a nodereference autocomplete element.
+ *
+ * Substitute in the node title for the node nid.
+ */
+function nr_autocomplete_value($element, $edit = FALSE) {
+  $names = array();
+  $field_key  = $element['#columns'][0];
+  foreach ($element['#default_value'] as $delta => $item) {
+    if (!empty($item['nid'])) {
+      $n = db_result(db_query(db_rewrite_sql('SELECT n.title FROM {node} n WHERE n.nid = %d'), $item['nid']));
+      $names[] = nr_autocomplete_encode($n) . ' [nid:'. $item['nid'] .']';
+    }
+  }
+  return array($field_key => implode(', ', $names));
+}
+
+/**
+ * Process an individual element.
+ *
+ * Build the form element. When creating a form using FAPI #process,
+ * note that $element['#value'] is already set.
+ *
+ */
+function nr_autocomplete_process($element, $edit, $form_state, $form) {
+  // The nodereference autocomplete widget doesn't need to create its own
+  // element, it can wrap around the text_textfield element and add an autocomplete
+  // path and some extra processing to it.
+  // Add a validation step where the value can be unwrapped.
+  $field_name = $element['#field_name'];
+  $field = $form['#field_info'][$field_name];
+
+  if (!$field['multiple']) {
+    $autocomplete = 'nodereference/autocomplete/';
+  }
+  else {
+    $autocomplete = 'nr_autocomplete/autocomplete/';
+  }
+  $field_key  = $element['#columns'][0];
+
+  $element[$field_key] = array(
+    '#type' => 'text_textfield',
+    '#default_value' => isset($element['#value']) ? $element['#value'] : '',
+    '#autocomplete_path' => $autocomplete . $element['#field_name'],
+    // The following values were set by the content module and need
+    // to be passed down to the nested element.
+    '#title' => $element['#title'],
+    '#required' => $element['#required'],
+    '#description' => $element['#description'],
+    '#field_name' => $element['#field_name'],
+    '#type_name' => $element['#type_name'],
+    '#delta' => $element['#delta'],
+    '#columns' => $element['#columns'],
+  );
+  if (empty($element[$field_key]['#element_validate'])) {
+    $element[$field_key]['#element_validate'] = array();
+  }
+  array_unshift($element[$field_key]['#element_validate'], 'nr_autocomplete_validate');
+
+  // Used so that hook_field('validate') knows where to flag an error.
+  $element['_error_element'] = array(
+    '#type' => 'value',
+    // Wrapping the element around a text_textfield element creates a
+    // nested element, so the final id will look like 'field-name-0-nid-nid'.
+    '#value' => implode('][', array_merge($element['#parents'], array($field_key, $field_key))),
+  );
+  return $element;
+}
+
+/**
+ * Validate an autocomplete element.
+ *
+ * Remove the wrapper layer and set the right element's value.
+ * This will move the nested value at 'field-name-0-nid-nid'
+ * back to its original location, 'field-name-0-nid'.
+ */
+function nr_autocomplete_validate($element, &$form_state) {
+  $field_name = $element['#field_name'];
+  $type_name = $element['#type_name'];
+  $field = content_fields($field_name, $type_name);
+  $field_key = $element['#columns'][0];
+  $value = $element['#value'][$field_key];
+  $values = array();
+  $nid = NULL;
+  $typed_titles = nr_autocomplete_parse_input($value);
+  foreach ($typed_titles as $delta => $item) {
+    if (!empty($item)) {
+      preg_match('/^(?:\s*|(.*) )?\[\s*nid\s*:\s*(\d+)\s*\]$/', $item, $matches);
+      if (!empty($matches)) {
+         // Explicit [nid:n].
+        list(, $title, $nid) = $matches;        
+        // Check for encoded and unencoded matches
+        if (!empty($title) && ($n = node_load($nid)) && nr_autocomplete_encode($title) != $n->title && $title != $n->title) {
+          form_error($element[$field_key], t('%name: title mismatch. Please check your selection.', array('%name' => t($field['widget']['label']))));
         }
-        elseif ($delta > 0) {
-          // Don't save empty fields when they're not the first value (keep '0' otherwise)
-          unset($items[$delta]);
+        else {
+          $values[] = array('nid' => $nid);
         }
       }
+    }    
+  }
+  $new_parents = array();
+  foreach ($element['#parents'] as $parent) {
+    $value = $value[$parent];
+    // Use === to be sure we get right results if parent is a zero (delta) value.
+    if ($parent === $field_key) {
+      $element['#parents'] = $new_parents;
+      form_set_value($element, $values, $form_state);
       break;
+    }
+    $new_parents[] = $parent;
+  }
+}
+
+/**
+ * Implementation of hook_allowed_values().
+ */
+function nr_autocomplete_allowed_values($field) {
+  $references = _nodereference_potential_references($field);
+
+  $options = array();
+  foreach ($references as $key => $value) {
+    $options[$key] = $value['rendered'];
+  }
+  return $options;
+}
+
+/**
+ * Retrieve a pipe delimited string of autocomplete suggestions
+ *
+ * Unholy union of taxonomy.module's taxonomy_autocomplete() and
+ * nodreference.module's nodreference_autocomplete().
+ */
+function nr_autocomplete_autocomplete($field_name, $string = '') {
+  // The user enters a comma-separated list of nodes. We only autocomplete the last node.
+  $array = drupal_explode_tags($string);
+  // Fetch last node
+  $last_string = trim(array_pop($array));
+
+  if ($last_string != '') {
+    $fields = content_fields();
+    $field = $fields[$field_name];
+    $count = count($array);
+    if ($count && empty($field['multiple'])) {
+      drupal_json(array());
+      return;
+    }
+    $match = isset($field['widget']['autocomplete_match']) ? $field['widget']['autocomplete_match'] : 'contains';
+    $matches = array();
+
+    $prefix = $count ? implode(', ', $array) . ', ' : '';
+
+    $references = _nodereference_potential_references($field, $last_string, $match, array(), 10);
+    foreach ($references as $id => $row) {
+      $n = nr_autocomplete_encode($row['title']) . " [nid:$id]";
+      // Add a class wrapper for a few required CSS overrides.
+      $matches[$prefix . $n] = '<div class="reference-autocomplete">'. $row['rendered'] . '</div>';
+    }
+    drupal_json($matches);
   }
 }
 
@@ -179,7 +286,7 @@ function nr_autocomplete_widget($op, &$n
 function nr_autocomplete_encode($string) {
   $n = $string;
   if (strpos($string, ',') !== FALSE || strpos($string, '"') !== FALSE) {
-    $n = '"'. str_replace('"', '""', $string) .'"';
+    $n = '"' . str_replace('"', '""', $string) . '"';
   }
   return $n;
 }
@@ -199,4 +306,10 @@ function nr_autocomplete_parse_input($ty
   preg_match_all($regexp, $typed_input, $matches);
   return array_unique($matches[1]);
 }
-?>
\ No newline at end of file
+
+/**
+ * Theme an individual textfield autocomplete element.
+ */
+function theme_nr_autocomplete($element) {
+  return $element['#children'];
+}
