array( 'arguments' => array('element' => NULL), ), ); } /** * Implementation of hook_menu */ function content_taxonomy_autocomplete_menu() { $items['content_taxonomy/autocomplete'] = array( 'title' => 'Autocomplete', 'page callback' => 'content_taxonomy_autocomplete_load', 'access arguments' => array('access content'), 'type' => MENU_CALLBACK ); return $items; } /** * Implementation of hook_widget_info(). */ function content_taxonomy_autocomplete_widget_info() { return array( 'content_taxonomy_autocomplete' => array( 'label' => t('Autocomplete (Freetagging)'), 'field types' => array('content_taxonomy'), 'multiple values' => CONTENT_HANDLE_MODULE, 'callbacks' => array( 'default value' => CONTENT_CALLBACK_DEFAULT, ), ), ); return $items; } /** * Implementation of hook_widget_settings */ function content_taxonomy_autocomplete_widget_settings($op, $widget) { switch ($op) { case 'form': $form['autocomplete'] = array( '#type' => 'fieldset', '#title' => t('Settings for Autocompletes'), '#collapsible' => TRUE, '#weight' => 10, ); $form['autocomplete']['new_terms'] = array( '#type' => 'radios', '#title' => t('Freetagging settings'), '#default_value' => isset($widget['new_terms']) ? $widget['new_terms'] : 'insert', '#options' => array('insert' => t('Allow and insert new terms by the user into the vocabulary'), 'deny' => t('Deny any new terms'), ), ); $form['autocomplete']['extra_parent'] = array( '#type' => 'select', '#title' => t('Extra Parent for new terms'), '#options' => _content_taxonomy_get_all_terms(), '#default_value' => (isset($widget['extra_parent']) && is_numeric($widget['extra_parent'])) ? $widget['extra_parent'] : 0, '#description' => t('This setting is only relevant if you have selected "Allow and insert new terms by the user into the vocabulary". If you select any term here, new terms will get children of the selected one, otherwise new terms get children of the parent term (root, if no parent selected) selected in the global settings.'), ); $form['autocomplete']['maxlength'] = array( '#type' => 'textfield', '#title' => t('Maximum length of autocomplete'), '#default_value' => (isset($widget['maxlength']) && is_numeric($widget['maxlength'])) ? $widget['maxlength'] : 255, '#element_validate' => array('_content_taxonomy_autocomplete_widget_settings_maxlength_validate'), '#required' => TRUE, '#description' => t('Defines how many characters can be typed into the autocomplete field. For values higher than 255, remember that one term name can not be longer than 255 (would be cutted), nevertheless it\'s not a problem for multiple values, separated by commas.'), ); if (module_exists('active_tags')) { $form['autocomplete']['active_tags'] = array( '#type' => 'checkbox', '#title' => t('Use Active Tags style widget'), '#default_value' => isset($widget['active_tags']) ? $widget['active_tags'] : 0, '#description' => t('Use the Active Tags module to improve the usability of this autocomplete widget.'), ); } return $form; case 'save': return array('new_terms', 'extra_parent', 'maxlength', 'active_tags'); } } function _content_taxonomy_autocomplete_widget_settings_maxlength_validate($element, &$form_state) { $value = $form_state['values']['maxlength']; if (!is_numeric($value) || intval($value) != $value || $value <= 0) { form_error($element, t('"Maximum length" must be a positive integer.')); } } /** * 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 content_taxonomy_autocomplete_elements() { return array( 'content_taxonomy_autocomplete' => array( '#input' => TRUE, '#columns' => array('value'), '#delta' => 0, '#process' => array('content_taxonomy_autocomplete_process'), '#autocomplete_path' => FALSE, ), ); } /** * Implementation of hook_widget(). */ function content_taxonomy_autocomplete_widget(&$form, &$form_state, $field, $items, $delta = NULL) { $element = array( '#type' => 'content_taxonomy_autocomplete', '#default_value' => isset($items) ? $items : NULL, '#value_callback' => 'content_taxonomy_autocomplete_value', '#vid' => $field['vid'], ); return $element; } /** * Value for a content taxonomy autocomplete field * * returns the taxonomy term name for term ids */ function content_taxonomy_autocomplete_value($element, $edit = FALSE) { $field_key = $element['#columns'][0]; $terms = array(); if (count($element['#default_value'])) { foreach ($element['#default_value'] as $delta => $entry) { $terms[] = taxonomy_get_term($entry[$field_key]); } } $value = content_taxonomy_autocomplete_merge_tags($terms, $element['#vid']); $value = !empty($value) ? $value : NULL; return array($field_key => $value); } /** * Process an individual element. * * Build the form element. When creating a form using FAPI #process, * note that $element['#value'] is already set. * */ function content_taxonomy_autocomplete_process($element, $edit, $form_state, $form) { $field_name = $element['#field_name']; $field = $form['#field_info'][$field_name]; $field_key = $element['#columns'][0]; $element[$field_key] = array( '#type' => 'textfield', '#default_value' => isset($element['#value'][$field_key]) ? $element['#value'][$field_key] : '', '#autocomplete_path' => 'content_taxonomy/autocomplete/'. $element['#field_name'], '#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'], '#maxlength' => !empty($field['widget']['maxlength']) ? $field['widget']['maxlength'] : 255, ); if (empty($element[$field_key]['#element_validate'])) { $element[$field_key]['#element_validate'] = array(); } array_unshift($element[$field_key]['#element_validate'], 'content_taxonomy_autocomplete_validate'); if (module_exists('active_tags') && $field['widget']['active_tags']) { active_tags_enable_widget('#' . $element['#id'] . '-value-wrapper'); } return $element; } /** * Validation function for the content_taxonomy_autocomplete element * * parses input, handles new terms (depending on settings) and sets the values as needed for storing the data */ function content_taxonomy_autocomplete_validate($element, &$form_state) { $field_name = $element['#field_name']; $field = content_fields($field_name, $element['#type_name']); $field_key = $element['#columns'][0]; //if the element parents array contains the field key, we have to remove it //because otherwise form_set_value won't work. (still the question why is it in) if ($element['#parents'][count($element['#parents'])-1] == $field_key) { array_pop($element['#parents']); array_pop($element['#array_parents']); } $value = $element['#value']; $extracted_ids = content_taxonomy_autocomplete_tags_get_tids($value, $field['vid'], content_taxonomy_field_get_parent($field), $field['widget']['extra_parent']); if (!$field['multiple'] && count(content_taxonomy_autocomplete_split_tags($value, $field['vid'])) > 1) { form_set_error($field['field_name'] .'][value', t('You can provide only one value')); return; } else if (($field['multiple'] >= 2) && (count(content_taxonomy_autocomplete_split_tags($value, $field['vid'])) > $field['multiple'])) { form_set_error($field['field_name'] .'][value', t('%name: this field cannot hold more than @count values.', array('%name' => t($field['widget']['label']), '@count' => $field['multiple']))); } if ($field['widget']['new_terms'] == 'deny') { if (is_array($extracted_ids['non_existing_terms'])) { form_set_error($field['field_name'] .'][value', t('New tags are not allowed')); return; } } $values = content_taxonomy_autocomplete_form2data($extracted_ids, $field, $element); form_set_value($element, $values, $form_state); } /** * Helper function to transpose the values returned by submitting the content_taxonomy_autcomplete * to the format to be stored in the field */ function content_taxonomy_autocomplete_form2data($extracted_ids, $field, $element) { $existing_tids = is_array($extracted_ids['existing_tids']) ? $extracted_ids['existing_tids'] : array(); $new_tids = array(); if (is_array($extracted_ids['non_existing_terms'])) { if ($field['widget']['extra_parent']) { $new_tids = content_taxonomy_autocomplete_insert_tags($extracted_ids['non_existing_terms'], $field['widget']['extra_parent']); } else { $new_tids = content_taxonomy_autocomplete_insert_tags($extracted_ids['non_existing_terms'], content_taxonomy_field_get_parent($field)); } } return content_transpose_array_rows_cols(array($element['#columns'][0] => array_merge($existing_tids, $new_tids))); } /** * Retrieve a pipe delimited string of autocomplete suggestions * * @param String Fieldname * @param Integer TID of a parent (optional) * @param BOOLEAN whether a multiple field or not * @param STRING typed input */ function content_taxonomy_autocomplete_load($field_name, $string = '') { // The user enters a comma-separated list of tags. We only autocomplete the last tag. // This regexp allows the following types of user input: // this, "somecmpany, llc", "and ""this"" w,o.rks", foo bar $content_type_info = _content_type_info(); $vid = $content_type_info['fields'][$field_name]['vid']; $tid = content_taxonomy_field_get_parent($content_type_info['fields'][$field_name]); // If the menu system has splitted the search text because of slashes, glue it back. if (func_num_args() > 2) { $args = func_get_args(); $string .= '/'. implode('/', array_slice($args, 2)); } // The user enters a comma-separated list of tags. We only autocomplete the last tag. $array = drupal_explode_tags($string); // Fetch last tag $last_string = trim(array_pop($array)); $matches = array(); if ($last_string != '') { if ($tid) { $result = db_query_range(db_rewrite_sql("SELECT t.name FROM {term_data} t LEFT JOIN {term_synonym} s ON t.tid = s.tid INNER JOIN {term_hierarchy} h ON t.tid = h.tid WHERE h.parent = %d AND (LOWER(t.name) LIKE LOWER('%%%s%%') OR LOWER(s.name) LIKE LOWER('%%%s%%'))", 't', 'tid'), $tid,$last_string,$last_string,0,10); } else { $result = db_query_range(db_rewrite_sql("SELECT t.name FROM {term_data} t LEFT JOIN {term_synonym} s ON t.tid = s.tid WHERE t.vid = %d AND (LOWER(t.name) LIKE LOWER('%%%s%%') OR LOWER(s.name) LIKE LOWER('%%%s%%'))", 't', 'tid'), $vid, $last_string, $last_string, 0, 10); } $prefix = count($array) ? '"'. implode('", "', $array) .'", ' : ''; while ($tag = db_fetch_object($result)) { $n = $tag->name; // Commas and quotes in terms are special cases, so encode 'em. if (strpos($tag->name, ',') !== FALSE || strpos($tag->name, '"') !== FALSE) { $n = '"'. str_replace('"', '""', $tag->name) .'"'; } $matches[$prefix . $n] = check_plain($tag->name); } } drupal_json($matches); } /** * Get TIDs for freetagging tags * Free tagging vocabularies do not send their tids in the form, * so we'll detect them here and process them independently. * @param $typed_input A string containing all comma separated tags. As the user typed it. */ function content_taxonomy_autocomplete_tags_get_tids($typed_input, $vid, $parent = 0, $extra_parent = 0) { // This regexp allows the following types of user input: // this, "somecmpany, llc", "and ""this"" w,o.rks", foo bar $typed_terms = content_taxonomy_autocomplete_split_tags($typed_input); foreach ($typed_terms as $typed_term) { // If a user has escaped a term (to demonstrate that it is a group, // or includes a comma or quote character), we remove the escape // formatting so to save the term into the DB as the user intends. $typed_term = trim(str_replace('""', '"', preg_replace('/^"(.*)"$/', '\1', $typed_term))); if ($typed_term == "") { continue; } // See if the term exists in the chosen vocabulary // and return the tid, otherwise, add a new record. $possibilities = taxonomy_get_term_by_name($typed_term); $typed_term_tid = NULL; // tid match if any. foreach ($possibilities as $possibility) { if ($possibility->vid == $vid) { if ($parent) { $parents = array(); $parents = taxonomy_get_parents($possibility->tid); if (in_array($parent, array_keys($parents)) || in_array($extra_parent, array_keys($parents))) { $result['existing_tids'][$possibility->tid] = $possibility->tid; $typed_term_tid = $possibility->tid; } } else { $result['existing_tids'][$possibility->tid] = $possibility->tid; $typed_term_tid = $possibility->tid; } } } if (!$typed_term_tid) { $result['non_existing_terms'][] = array( 'name' => $typed_term, 'vid' => $vid, ); } } return $result; } /** * Insert new tags * * @param $nid the node id * @param $terms an array of all nonexisting terms. * @return an array of newly inserted term ids */ function content_taxonomy_autocomplete_insert_tags($terms, $parent = NULL) { foreach ($terms as $term) { $edit = array('vid' => $term['vid'], 'name' => $term['name']); if ($parent) $edit['parent'] = $parent; $status = taxonomy_save_term($edit); $saved_terms[$edit['tid']] = $edit['tid']; } return $saved_terms; } /** * Helper function to split the tags */ function content_taxonomy_autocomplete_split_tags($typed_input) { $regexp = '%(?:^|,\ *)("(?>[^"]*)(?>""[^"]* )*"|(?: [^",]*))%x'; preg_match_all($regexp, $typed_input, $matches); return $matches[1]; } /** * Helper function to merge the tags, to prefill the fields when editing a node. */ function content_taxonomy_autocomplete_merge_tags($terms, $vid) { $typed_terms = array(); if (!empty($terms)) { foreach ($terms as $term) { // Extract terms belonging to the vocabulary in question. if ($term->vid == $vid) { //if ($tid && in_array($term->tid,drupal_map_assoc(array_keys((taxonomy_get_children($tid,$vid)))))) { // Commas and quotes in terms are special cases, so encode 'em. $name = $term->name; if (preg_match('/,/', $term->name) || preg_match('/"/', $term->name)) { $name = '"'. preg_replace('/"/', '""', $name) .'"'; } $typed_terms[] = $name; // } } } } return implode(', ', $typed_terms); } function theme_content_taxonomy_autocomplete($element) { return $element['#children']; }