? 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 @@ '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 Contains 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] = '