diff options
author | Angie Byron <webchick@24967.no-reply.drupal.org> | 2009-08-18 06:01:07 +0000 |
---|---|---|
committer | Angie Byron <webchick@24967.no-reply.drupal.org> | 2009-08-18 06:01:07 +0000 |
commit | ae1d9c577baf30ce74c95ea557809b35d77b0806 (patch) | |
tree | 7edafb1fd6f344028152ee17a934f59dc3088951 | |
parent | 80b4cd6610432fd1a8c29d747127b1480a2d2394 (diff) | |
download | brdo-ae1d9c577baf30ce74c95ea557809b35d77b0806.tar.gz brdo-ae1d9c577baf30ce74c95ea557809b35d77b0806.tar.bz2 |
#526122 by bangpound, yched, and catch: Added an autocomplete widget for taxonomy term fields.
-rw-r--r-- | modules/node/node.pages.inc | 1 | ||||
-rw-r--r-- | modules/taxonomy/taxonomy.module | 197 | ||||
-rw-r--r-- | modules/taxonomy/taxonomy.pages.inc | 57 |
3 files changed, 252 insertions, 3 deletions
diff --git a/modules/node/node.pages.inc b/modules/node/node.pages.inc index 56b210df7..adcd326d4 100644 --- a/modules/node/node.pages.inc +++ b/modules/node/node.pages.inc @@ -330,6 +330,7 @@ function theme_node_form($form) { */ function node_preview($node) { if (node_access('create', $node) || node_access('update', $node)) { + _field_invoke_multiple('load', 'node', array($node->nid => $node)); // Load the user's name when needed. if (isset($node->name)) { // The use of isset() is mandatory in the context of user IDs, because diff --git a/modules/taxonomy/taxonomy.module b/modules/taxonomy/taxonomy.module index f78a409b6..c68056337 100644 --- a/modules/taxonomy/taxonomy.module +++ b/modules/taxonomy/taxonomy.module @@ -84,6 +84,9 @@ function taxonomy_theme() { 'field_formatter_taxonomy_term_plain' => array( 'arguments' => array('element' => NULL), ), + 'taxonomy_autocomplete' => array( + 'arguments' => array('element' => NULL), + ), ); } @@ -228,6 +231,13 @@ function taxonomy_menu() { 'access arguments' => array('access content'), 'type' => MENU_CALLBACK, ); + // TODO: remove with taxonomy_term_node_* + $items['taxonomy/autocomplete/legacy'] = array( + 'title' => 'Autocomplete taxonomy', + 'page callback' => 'taxonomy_autocomplete_legacy', + 'access arguments' => array('access content'), + 'type' => MENU_CALLBACK, + ); $items['admin/structure/taxonomy/%taxonomy_vocabulary'] = array( 'title' => 'Vocabulary', // this is replaced by callback @@ -689,7 +699,7 @@ function taxonomy_form_alter(&$form, $form_state, $form_id) { '#description' => $help, '#required' => $vocabulary->required, '#default_value' => $typed_string, - '#autocomplete_path' => 'taxonomy/autocomplete/' . $vocabulary->vid, + '#autocomplete_path' => 'taxonomy/autocomplete/legacy/' . $vocabulary->vid, '#weight' => $vocabulary->weight, '#maxlength' => 1024, ); @@ -1841,6 +1851,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) { @@ -1874,6 +1913,19 @@ function taxonomy_field_schema($field) { */ function taxonomy_field_validate($obj_type, $object, $field, $instance, $items, &$errors) { $allowed_values = taxonomy_allowed_values($field); + $widget = field_info_widget_types($instance['widget']['type']); + + // Check we don't exceed the allowed number of values for widgets with custom + // behavior for multiple values (taxonomy_autocomplete widget). + if ($widget['behaviors']['multiple values'] == FIELD_BEHAVIOR_CUSTOM && $field['cardinality'] >= 2) { + if (count($items) > $field['cardinality']) { + $errors[$field['field_name']][0][] = array( + 'error' => 'taxonomy_term_illegal_value', + 'message' => t('%name: this field cannot hold more that @count values.', array('%name' => t($instance['label']), '@count' => $field['cardinality'])), + ); + } + } + foreach ($items as $delta => $item) { if (!empty($item['value'])) { if (!isset($allowed_values[$item['value']])) { @@ -2052,3 +2104,146 @@ function _taxonomy_clean_field_cache($term) { 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 autocomplete widget 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) { + $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']; + + $value = array(); + $element[$field_key] = array( + '#type' => 'textfield', + '#default_value' => $typed_string, + '#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) { + // Autocomplete widgets do not send their tids in the form, so we must 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']; + } + $typed_terms = drupal_explode_tags($form_state['values'][$element['#field_name']]['value']); + $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; + break; + } + if (!$typed_term_tid) { + $vocabulary = taxonomy_vocabulary_load($vids[0]); + $edit = array( + 'vid' => $vids[0], + 'name' => $typed_term, + 'vocabulary_machine_name' => $vocabulary->machine_name, + ); + $term = (object) $edit; + if ($status = taxonomy_term_save($term)) { + $typed_term_tid = $term->tid; + } + } + $values[$typed_term_tid] = $typed_term_tid; + } + $results = options_transpose_array_rows_cols(array($field_key => $values)); + 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'), + ), + ); +} diff --git a/modules/taxonomy/taxonomy.pages.inc b/modules/taxonomy/taxonomy.pages.inc index 983ea3ac9..7d50436da 100644 --- a/modules/taxonomy/taxonomy.pages.inc +++ b/modules/taxonomy/taxonomy.pages.inc @@ -90,7 +90,7 @@ function taxonomy_term_edit($term) { /** * Helper function for autocompletion */ -function taxonomy_autocomplete($vid = 0, $tags_typed = '') { +function taxonomy_autocomplete_legacy($vid = 0, $tags_typed = '') { // 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)); @@ -131,11 +131,64 @@ function taxonomy_autocomplete($vid = 0, $tags_typed = '') { $name = t('Did you mean %suggestion', array('%suggestion' => $name)); $synonym_matches[$prefix . $n] = filter_xss($name); } + } + } + + drupal_json(array_merge($term_matches, $synonym_matches)); +} + +/** + * Helper function for autocompletion + */ +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']; + } + + $query = db_select('taxonomy_term_data', 't'); + $query->addTag('term_access'); + + // Do not select already entered terms. + if (!empty($tags_typed)) { + $query->condition('t.name', $tags_typed, 'NOT IN'); + } + $tags_return = $query + ->fields('t', array('tid', 'name')) + ->condition('t.vid', $vids) + // Select rows that match by term name. + ->condition(db_or() + ->where("t.name LIKE :last_string", array(':last_string' => '%' . $tag_last . '%')) + ) + ->range(0, 10) + ->execute() + ->fetchAllKeyed(); + + $prefix = count($tags_typed) ? implode(', ', $tags_typed) . ', ' : ''; + + $term_matches = array(); + foreach ($tags_return as $tid => $name) { + $n = $name; + // Term names containing commas or quotes must be wrapped in quotes. + if (strpos($name, ',') !== FALSE || strpos($name, '"') !== FALSE) { + $n = '"' . str_replace('"', '""', $name) . '"'; + } else { $term_matches[$prefix . $n] = filter_xss($name); } } } - drupal_json(array_merge($term_matches, $synonym_matches)); + drupal_json($term_matches); } |