You are here

shs.module in Simple hierarchical select 7

Same filename and directory in other branches
  1. 8 shs.module
  2. 2.0.x shs.module

Provides an additional widget for term fields to create hierarchical selects.

File

shs.module
View source
<?php

/**
 * @file
 * Provides an additional widget for term fields to create hierarchical selects.
 */

/**
 * Implements hook_menu().
 */
function shs_menu() {
  $items = array();

  // Create menu item for JSON callbacks.
  $items['js/shs'] = array(
    'title' => 'JSON callback',
    'description' => 'JSON callbacks for Simple hierarchical select',
    'page callback' => 'shs_json',
    'access callback' => 'user_access',
    'access arguments' => array(
      'access content',
    ),
    'type' => MENU_CALLBACK,
  );
  return $items;
}

/**
 * Implements hook_js().
 */
function shs_js() {
  $settings = array(
    'json' => array(
      'callback' => 'shs_json',
      'access callback' => 'user_access',
      'access arguments' => array(
        'access content',
      ),
      'includes' => array(
        'path',
      ),
      'dependencies' => array(
        'taxonomy',
        'field',
        'field_sql_storage',
      ),
    ),
  );
  drupal_alter('shs_js_info', $settings);
  return $settings;
}

/**
 * Menu callback to get data in JSON format.
 */
function shs_json() {
  $result = array(
    'success' => FALSE,
    'data' => array(),
  );
  if (isset($_POST['callback'])) {

    // Get name of function we need to call to get the data.
    $_callback = check_plain($_POST['callback']);

    // Is this a valid callback?
    $valid_callbacks = shs_json_callbacks();
    if (isset($valid_callbacks[$_callback]) && !empty($valid_callbacks[$_callback]['callback']) && function_exists($valid_callbacks[$_callback]['callback'])) {

      // Get arguments and validate them.
      $post_args = isset($_POST['arguments']) && is_array($_POST['arguments']) ? $_POST['arguments'] : array();
      $arguments = _shs_json_callback_get_arguments($valid_callbacks[$_callback], $post_args);
      if (($callback_result = call_user_func_array($valid_callbacks[$_callback]['callback'], $arguments)) !== FALSE) {
        $result['success'] = TRUE;
        $result['data'] = $callback_result;
      }
    }
  }

  // Return result as JSON string.
  drupal_json_output($result);
}

/**
 * Get a list of supported JSON callbacks.
 *
 * @return array
 *   List of valid callbacks with the following structure:
 *   - [name of callback]
 *     - 'callback': function to call
 *     - 'arguments'
 *       - [name of argument]: [validation function] (FALSE for no validation)
 */
function shs_json_callbacks() {
  $callbacks = array(
    'shs_json_term_get_children' => array(
      'callback' => 'shs_json_term_get_children',
      'arguments' => array(
        'vid' => 'shs_json_validation_vocabulary_identifier',
        'parent' => 'is_array',
        'settings' => 'is_array',
        'field' => 'is_string',
      ),
    ),
    'shs_json_term_add' => array(
      'callback' => 'shs_json_term_add',
      'arguments' => array(
        'token' => array(
          'callback' => 'shs_json_valid_token',
          'arguments' => array(
            'field_name' => '@field',
          ),
        ),
        'vid' => 'shs_json_validation_vocabulary_identifier',
        'parent' => 'is_numeric',
        'name' => 'is_string',
        'field' => 'is_string',
      ),
    ),
  );

  // Let other modules add some more callbacks and alter the existing.
  drupal_alter('shs_json_callbacks', $callbacks);
  return $callbacks;
}

/**
 * Wrapper around drupal_valid_token().
 *
 * @param string $token
 *   The token to validate.
 * @param array $params
 *   Additional params to generate the token key.
 *   - field_name: (required) Name of field the token has been generated for.
 *
 * @return bool
 *   TRUE if the token is valid, FALSE otherwise.
 *
 * @see drupal_valid_token()
 */
function shs_json_valid_token($token, array $params) {
  if (empty($params['field_name'])) {
    $t_args = array(
      '%param' => 'field_name',
      '%function' => 'shs_json_valid_token',
    );
    watchdog('shs', 'Missing mandantory token parameter "%param" in %function', $t_args, WATCHDOG_ERROR);
    return FALSE;
  }
  $token_key = 'shs-' . $params['field_name'];
  return drupal_valid_token($token, $token_key);
}

/**
 * Helper function to get the (validated) arguments for a JSON callback.
 *
 * @param array $callback
 *   Callback definition from shs_json_callbacks().
 * @param array $arguments
 *   Unfiltered arguments posted with $.ajax().
 *
 * @return array
 *   List of (validated) arguments for this callback. Any arguments not defined
 *   for this callback will be removed.
 */
function _shs_json_callback_get_arguments(array $callback, array $arguments) {
  $result = array();

  // Get arguments from callback definition.
  $callback_arguments = $callback['arguments'];
  foreach ($arguments as $key => $value) {
    if (!isset($callback_arguments[$key])) {
      continue;
    }
    $argument_valid = TRUE;
    if (($validation_function = $callback_arguments[$key]) === FALSE) {
      $argument_valid = FALSE;
      continue;
    }
    $validation_arguments = array();
    if (is_array($validation_function)) {
      if (!isset($validation_function['callback'])) {
        $argument_valid = FALSE;
        watchdog('shs', 'Invalid structure for shs_json validation callback %key', array(
          '%key' => $key,
        ), WATCHDOG_ERROR);

        // Stop validation right now.
        return $result;
      }
      foreach ($validation_function['arguments'] as $validation_argument_key => $validation_argument) {
        if (strpos($validation_argument, '@') === 0) {

          // Back-reference to callback argument.
          $argument_name = substr($validation_argument, 1);
          if (isset($arguments[$argument_name])) {
            $validation_arguments[$validation_argument_key] = $arguments[$argument_name];
          }
        }
        else {
          $validation_arguments[$validation_argument_key] = $validation_argument;
        }
      }
      $validation_function = $validation_function['callback'];
    }
    if (function_exists($validation_function)) {

      // Validate argument.
      if (empty($validation_arguments)) {
        $argument_valid = call_user_func($validation_function, $value);
      }
      else {
        $argument_valid = call_user_func($validation_function, $value, $validation_arguments);
      }
    }
    if ($argument_valid) {

      // Add argument and its value to the result list.
      $result[$key] = $value;
    }
  }
  return $result;
}

/**
 * Implements hook_views_data_alter().
 */
function shs_views_data_alter(&$data) {

  // Add filter handler for term ID with depth.
  $data['node']['shs_term_node_tid_depth'] = array(
    'help' => t('Display content if it has the selected taxonomy terms, or children of the selected terms. Due to additional complexity, this has fewer options than the versions without depth. Optionally the filter will use a simple hierarchical select for the selection of terms.'),
    'real field' => 'nid',
    'filter' => array(
      'title' => t('Has taxonomy terms (with depth; @type)', array(
        '@type' => 'Simple hierarchical select',
      )),
      'handler' => 'shs_handler_filter_term_node_tid_depth',
    ),
  );
}

/**
 * Implements hook_field_views_data_alter().
 */
function shs_field_views_data_alter(&$result, &$field, $module) {
  if (empty($field['columns']) || !in_array($field['type'], array(
    'taxonomy_term_reference',
    'entityreference',
  ))) {
    return;
  }
  if ($field['type'] == 'entityreference' && (empty($field['settings']['target_type']) || $field['settings']['target_type'] != 'taxonomy_term')) {

    // Do not change entityreference fields that do not reference terms.
    return;
  }
  $field_column = key($field['columns']);
  foreach ($result as $key => $group) {
    $field_identifier = sprintf('%s_%s', $field['field_name'], $field_column);
    if (empty($group[$field_identifier]) || empty($group[$field_identifier]['filter']['handler'])) {

      // Only modify field definitions for the primary column.
      continue;
    }

    // Replace handler.
    $result[$key][$field_identifier]['filter']['handler'] = $field['type'] == 'entityreference' ? 'shs_handler_filter_entityreference' : 'shs_handler_filter_term_node_tid';
  }
}

/**
 * Implements hook_conditional_fields_states_handlers_alter().
 */
function shs_conditional_fields_states_handlers_alter(&$handlers) {
  $handlers += array(
    'shs_conditional_fields_states_handler_shs' => array(
      array(
        'tid' => array(
          '#type' => 'select',
        ),
      ),
    ),
  );
}

/**
 * States handler for simple hierarchical selects.
 */
function shs_conditional_fields_states_handler_shs($field, $field_info, &$options, &$state) {
  switch ($options['values_set']) {
    case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_WIDGET:
    case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_AND:
      if (empty($state[$options['state']][$options['selector']])) {
        return;
      }
      $old_state = $state[$options['state']][$options['selector']];
      if (isset($old_state['value'][0]['tid'])) {

        // Completly remove old state.
        unset($state[$options['state']][$options['selector']]);
        $options['selector'] .= '-0-tid';
        $state[$options['state']][$options['selector']] = $old_state;
        $state[$options['state']][$options['selector']]['value'] = $old_state['value'][0]['tid'];
      }
      return;
    case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_XOR:
      $select_states[$options['state']][] = 'xor';
    case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_REGEX:
      $regex = TRUE;
    case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_NOT:
    case CONDITIONAL_FIELDS_DEPENDENCY_VALUES_OR:
      foreach ($options['values'] as $value) {
        $select_states[$options['state']][] = array(
          $options['selector'] => array(
            $options['condition'] => empty($regex) ? array(
              $value,
            ) : $options['value'],
          ),
        );
      }
      break;
  }
  $state = $select_states;
}

/**
 * Implements hook_field_widget_info().
 */
function shs_field_widget_info() {
  return array(
    'taxonomy_shs' => array(
      'label' => t('Simple hierarchical select'),
      'field types' => array(
        'taxonomy_term_reference',
        'entityreference',
      ),
      'settings' => array(
        'shs' => array(
          'create_new_terms' => FALSE,
          'create_new_levels' => FALSE,
          'force_deepest' => FALSE,
        ),
      ),
    ),
  );
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function shs_form_field_ui_field_settings_form_alter(&$form, &$form_state, $form_id) {
  if (module_exists('entityreference') && $form['field']['type']['#value'] == 'entityreference' && $form['field']['settings']['#instance']['widget']['type'] == 'taxonomy_shs') {
    $form['field']['settings']['#field']['settings']['target_type'] = 'taxonomy_term';
    $form['field']['settings']['#process'][] = 'shs_entityreference_field_settings_process';
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function shs_form_field_ui_field_edit_form_alter(&$form, &$form_state, $form_id) {
  if (module_exists('entityreference') && $form['#field']['type'] == 'entityreference' && $form['#instance']['widget']['type'] == 'taxonomy_shs') {
    $form['#field']['settings']['target_type'] = 'taxonomy_term';
    $form['field']['settings']['#process'][] = 'shs_entityreference_field_settings_process';
  }
}

/**
 * Additional processing function for the entityreference field settings form.
 *
 * @param array $form
 *   Form structure to process.
 * @param array $form_state
 *   Current form state.
 *
 * @return array
 *   Processed form structure.
 */
function shs_entityreference_field_settings_process(array $form, array $form_state) {
  if (!empty($form['target_type'])) {

    // Reduce list of available target types to taxonomy terms.
    $form['target_type']['#options'] = array(
      'taxonomy_term' => t('Taxonomy term'),
    );
  }
  return $form;
}

/**
 * Implements hook_field_widget_settings_form().
 */
function shs_field_widget_settings_form($field, $instance) {
  $widget = $instance['widget'];
  $settings = $widget['settings'];
  $form = array();
  $form['shs'] = array(
    '#type' => 'fieldset',
    '#title' => 'Simple hierarchical select settings',
    '#collapsible' => TRUE,
    '#collapsed' => FALSE,
    '#tree' => TRUE,
  );
  if ($field['type'] != 'entityreference' || $field['type'] == 'entityreference' && !empty($field['settings']['handler_settings']['target_bundles']) && count($field['settings']['handler_settings']['target_bundles']) == 1) {
    $form['shs']['create_new_terms'] = array(
      '#type' => 'checkbox',
      '#title' => t('Allow creating new terms'),
      '#description' => t('If checked the user will be able to create new terms (permission to edit terms in this vocabulary must be set).'),
      '#default_value' => empty($settings['shs']['create_new_terms']) ? FALSE : $settings['shs']['create_new_terms'],
    );
    $form['shs']['create_new_levels'] = array(
      '#type' => 'checkbox',
      '#title' => t('Allow creating new levels'),
      '#description' => t('If checked the user will be able to create new children for items which do not have any children yet (permission to edit terms in this vocabulary must be set).'),
      '#default_value' => empty($settings['shs']['create_new_levels']) ? FALSE : $settings['shs']['create_new_levels'],
      '#states' => array(
        'visible' => array(
          ':input[name="instance[widget][settings][shs][create_new_terms]"]' => array(
            'checked' => TRUE,
          ),
        ),
      ),
    );
  }
  $form['shs']['force_deepest'] = array(
    '#type' => 'checkbox',
    '#title' => t('Force selection of deepest level'),
    '#description' => t('If checked the user will be forced to select terms from the deepest level.'),
    '#default_value' => empty($settings['shs']['force_deepest']) ? FALSE : $settings['shs']['force_deepest'],
  );

  // "Chosen" integration.
  if (module_exists('chosen')) {
    $form['shs']['use_chosen'] = array(
      '#type' => 'select',
      '#title' => t('Output this field with !chosen', array(
        '!chosen' => l(t('Chosen'), 'http://drupal.org/project/chosen'),
      )),
      '#description' => t('Select in which cases the element will use the !chosen module for the term selection of each level.', array(
        '!chosen' => l(t('Chosen'), 'http://drupal.org/project/chosen'),
      )),
      '#default_value' => empty($settings['shs']['use_chosen']) ? 'chosen' : $settings['shs']['use_chosen'],
      '#options' => array(
        'chosen' => t('let chosen decide'),
        'always' => t('always'),
        'never' => t('never'),
      ),
    );
  }
  return $form;
}

/**
 * Implements hook_field_widget_error().
 */
function shs_field_widget_error($element, $error, $form, &$form_state) {
  form_error($element, $error['message']);
}

/**
 * Implements hook_field_widget_form().
 */
function shs_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
  $field_column = key($field['columns']);

  // Get value.
  $element_value = NULL;
  $submitted_value = NULL;
  if (!empty($form_state['values']) && !empty($element['#parents'])) {
    $submitted_value = drupal_array_get_nested_value($form_state['values'], $element['#parents']);
  }
  if (!empty($items[$delta][$field_column])) {

    // Use saved value from database or cache.
    $element_value = $items[$delta][$field_column];
  }
  elseif (!empty($submitted_value)) {

    // Use value from form_state (for example for fields with cardinality = -1).
    $element_value = array(
      'tid' => $submitted_value,
    );
  }

  // Get vocabulary names from allowed values.
  if ($field['type'] == 'entityreference') {
    if ('views' === $field['settings']['handler']) {
      $vocabulary_names = array();
      $view_settings = $field['settings']['handler_settings']['view'];

      // Try to load vocabularies from view filter.
      $vocabulary_names = _shs_entityreference_views_get_vocabularies($view_settings['view_name'], $view_settings['display_name']);
    }
    else {
      $vocabulary_names = $field['settings']['handler_settings']['target_bundles'];
    }
  }
  else {
    $allowed_values = reset($field['settings']['allowed_values']);
    $vocabulary_names = empty($allowed_values['vocabulary']) ? FALSE : $allowed_values['vocabulary'];
  }
  if (empty($vocabulary_names) && (empty($field['settings']['handler']) || 'views' !== $field['settings']['handler'])) {

    // No vocabulary selected yet.
    return array();
  }
  if (!is_array($vocabulary_names)) {
    $vocabulary_names = array(
      $vocabulary_names,
    );
  }
  $vocabularies = array();
  foreach ($vocabulary_names as $vocabulary_name) {
    if (($vocabulary = taxonomy_vocabulary_machine_name_load($vocabulary_name)) === FALSE) {

      // Vocabulary not found. Stop here.
      return array();
    }
    $vocabularies[] = $vocabulary;
  }

  // Check if term exists (the term could probably be deleted meanwhile).
  if ($element_value && taxonomy_term_load($element_value) === FALSE) {
    $element_value = 0;
  }
  if (count($vocabularies) > 1 || !isset($vocabulary) || isset($vocabulary) && !user_access('edit terms in ' . $vocabulary->vid)) {

    // Creating new terms is allowed only with proper permission and if only one
    // vocabulary is selected as source.
    $instance['widget']['settings']['shs']['create_new_terms'] = FALSE;
  }
  $instance['widget']['settings']['shs']['test_create_new_terms'] = module_implements('shs_add_term_access');
  $instance['widget']['settings']['shs']['required'] = $element['#required'];

  // Prepare the list of options.
  if ($field['type'] == 'entityreference') {

    // Get current selection handler.
    $handler = entityreference_get_selection_handler($field, $instance, $element['#entity_type'], $element['#entity']);
    $referencable_entities = $handler
      ->getReferencableEntities();
    $options = array(
      '_none' => empty($element['#required']) ? t('- None -', array(), array(
        'context' => 'shs',
      )) : t('- Select a value -', array(), array(
        'context' => 'shs',
      )),
    );
    foreach ($referencable_entities as $terms) {
      $options += $terms;
    }
  }
  else {
    $properties = _options_properties('select', FALSE, $element['#required'], !empty($element_value));
    $options = _options_get_options($field, $instance, $properties, $element['#entity_type'], $element['#entity']);
  }

  // Create element.
  $element += array(
    '#type' => 'select',
    '#default_value' => empty($element_value) ? NULL : $element_value,
    '#options' => $options,
    '#attributes' => array(
      'class' => array(
        'shs-enabled',
      ),
    ),
    // Prevent errors with drupal_strlen().
    '#maxlength' => NULL,
    '#element_validate' => array(
      'shs_field_widget_validate',
    ),
    '#after_build' => array(
      'shs_field_widget_afterbuild',
    ),
    '#shs_settings' => $instance['widget']['settings']['shs'],
    '#shs_vocabularies' => $vocabularies,
  );
  $return = array(
    $field_column => $element,
  );
  if (!empty($element['#title'])) {

    // Set title to "parent" element to enable label translation.
    $return['#title'] = $element['#title'];
  }
  return $return;
}

/**
 * Afterbuild callback for widgets of type "taxonomy_shs".
 */
function shs_field_widget_afterbuild($element, &$form_state) {
  $js_added =& drupal_static(__FUNCTION__ . '_js_added', array());

  // Generate a random hash to avoid merging of settings by drupal_add_js.
  // This is necessary until http://drupal.org/node/208611 lands for D7.
  $js_hash =& drupal_static(__FUNCTION__ . '_js_hash');
  if (empty($js_hash)) {
    $js_hash = _shs_create_hash();
  }
  $parents = array();

  // Get default value from form state and set it to element.
  $default_value = drupal_array_get_nested_value($form_state['values'], $element['#parents']);
  if (!empty($default_value)) {

    // Use value from form_state (for example for fields with cardinality = -1).
    $element['#default_value'] = $default_value;
  }

  // Add main Javascript behavior and style only once.
  if (count($js_added) == 0) {

    // Add behavior.
    drupal_add_js(drupal_get_path('module', 'shs') . '/js/shs.js');

    // Add styles.
    drupal_add_css(drupal_get_path('module', 'shs') . '/theme/shs.form.css');
  }

  // Create Javascript settings for the element only if it hasn't been added
  // before.
  if (empty($js_added[$element['#name']][$js_hash])) {
    $element_value = $element['#default_value'];

    // Ensure field is rendered if it is required but not selected.
    if (empty($element_value) || $element_value == '_none') {

      // Add fake parent for new items or field required submit fail.
      $parents[] = array(
        'tid' => 0,
      );
    }
    else {
      $term_parents = taxonomy_get_parents_all($element_value);
      foreach ($term_parents as $term) {

        // Create term lineage.
        $parents[] = array(
          'tid' => $term->tid,
        );
      }
    }
    $vocabularies = $element['#shs_vocabularies'];
    $vocabulary_identifier = NULL;
    if (count($vocabularies) == 1) {

      // Get ID from first (and only) vocabulary.
      $vocabulary_identifier = $vocabularies[0]->vid;
    }
    else {
      $vocabulary_identifier = array(
        'field_name' => $element['#field_name'],
      );
    }

    // Create token to prevent against CSRF attacks.
    $token = drupal_get_token('shs-' . $element['#name']);

    // Create settings needed for our js magic.
    $settings_js = array(
      'shs' => array(
        "{$element['#name']}" => array(
          $js_hash => array(
            'vid' => $vocabulary_identifier,
            'settings' => $element['#shs_settings'],
            'default_value' => $element['#default_value'],
            'parents' => array_reverse($parents),
            'any_label' => empty($element['#required']) ? t('- None -', array(), array(
              'context' => 'shs',
            )) : t('- Select a value -', array(), array(
              'context' => 'shs',
            )),
            'any_value' => '_none',
            'token' => $token,
          ),
        ),
      ),
    );

    // Allow other modules to alter these settings.
    drupal_alter(array(
      'shs_js_settings',
      "shs_{$element['#field_name']}_js_settings",
    ), $settings_js, $element['#field_name'], $vocabulary_identifier);

    // Add settings.
    drupal_add_js($settings_js, 'setting');
    if (empty($js_added[$element['#name']])) {
      $js_added[$element['#name']] = array();
    }
    $js_added[$element['#name']][$js_hash] = TRUE;
  }
  unset($element['#needs_validation']);
  return $element;
}

/**
 * Validation handler for widgets of type "taxonomy_shs".
 */
function shs_field_widget_validate($element, &$form_state, $form) {
  $field_name = $element['#field_name'];
  $instance = field_widget_instance($element, $form_state);
  if (empty($instance['widget'])) {
    return;
  }

  // Load default settings.
  $settings = empty($instance['widget']['settings']['shs']) ? array() : $instance['widget']['settings']['shs'];
  if (!empty($element['#shs_settings'])) {

    // Use settings directly applied to widget (possibly overridden in
    // hook_field_widget_form_alter() or
    // hook_field_widget_WIDGET_TYPE_form_alter()).
    $settings = $element['#shs_settings'];
  }

  // Do we want to force the user to select terms from the deepest level?
  $force_deepest_level = empty($settings['force_deepest']) ? FALSE : $settings['force_deepest'];
  $field = field_widget_field($element, $form_state);
  $value = empty($element['#value']) ? '_none' : $element['#value'];
  if ($value == '_none') {
    unset($element['#value']);
    form_set_value($element, NULL, $form_state);
  }
  if ($element['#required'] && $value == '_none') {
    $element_name = empty($element['#title']) ? $instance['label'] : $element['#title'];
    form_error($element, t('!name field is required.', array(
      '!name' => $element_name,
    )));
    return;
  }
  if ($force_deepest_level && $value) {

    // Get vocabulary names from allowed values.
    if ($field['type'] == 'entityreference') {
      $vocabulary_names = $field['settings']['handler_settings']['target_bundles'];
    }
    else {
      $allowed_values = reset($field['settings']['allowed_values']);
      $vocabulary_names = empty($allowed_values['vocabulary']) ? FALSE : $allowed_values['vocabulary'];
    }
    if (empty($vocabulary_names)) {

      // No vocabulary selected yet.
      form_error($element, t('No vocabulary is configured as source for field %field_name.', array(
        '%field_name' => $instance['label'],
        '%field_machine_name' => $field_name,
      )));
    }
    if (!is_array($vocabulary_names)) {
      $vocabulary_names = array(
        $vocabulary_names,
      );
    }
    foreach ($vocabulary_names as $vocabulary_name) {
      if (($vocabulary = taxonomy_vocabulary_machine_name_load($vocabulary_name)) === FALSE) {

        // Vocabulary not found. Stop here.
        $t_args = array(
          '%machine_name' => $vocabulary_name,
          '%field_name' => $instance['label'],
          '%field_machine_name' => $field_name,
        );
        form_error($element, t('Vocabulary %machine_name is configured as source for field %field_name but could not be found.', $t_args));
        return;
      }

      // Does the selected term has any children?
      $children = shs_term_get_children($vocabulary->vid, $value);
      if (count($children)) {
        form_error($element, t('You need to select a term from the deepest level in field %field_name.', array(
          '%field_name' => $instance['label'],
          '%field_machine_name' => $field_name,
        )));
        return;
      }
    }
  }
}

/**
 * Implements hook_field_formatter_info().
 */
function shs_field_formatter_info() {
  return array(
    'shs_default' => array(
      'label' => t('Simple hierarchy'),
      'field types' => array(
        'taxonomy_term_reference',
        'entityreference',
      ),
      'settings' => array(
        'linked' => FALSE,
      ),
    ),
  );
}

/**
 * Implements hook_field_formatter_settings_form().
 */
function shs_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
  $display = $instance['display'][$view_mode];
  $settings = $display['settings'];
  $element = array();
  if ($display['type'] == 'shs_default') {
    $element['linked'] = array(
      '#title' => t('Link to term page'),
      '#type' => 'checkbox',
      '#default_value' => $settings['linked'],
    );
  }
  return $element;
}

/**
 * Implements hook_field_formatter_settings_summary().
 */
function shs_field_formatter_settings_summary($field, $instance, $view_mode) {
  $display = $instance['display'][$view_mode];
  $settings = $display['settings'];
  $summary = '';
  if ($display['type'] == 'shs_default') {
    $summary = t('Linked to term page: !linked', array(
      '!linked' => $settings['linked'] ? t('Yes') : t('No'),
    ));
  }
  return $summary;
}

/**
 * Implements hook_field_formatter_prepare_view().
 */
function shs_field_formatter_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items, $displays) {
  $field_column = key($field['columns']);
  foreach ($entities as $entity_id => $entity) {
    if (empty($instances[$entity_id]['widget']['type']) || $instances[$entity_id]['widget']['type'] != 'taxonomy_shs') {
      return;
    }
    foreach ($items[$entity_id] as $delta => $item) {
      $items[$entity_id][$delta]['parents'] = array();

      // Load list of parent terms.
      $parents = taxonomy_get_parents_all($item[$field_column]);

      // Remove current term from list.
      array_shift($parents);
      if (module_exists('i18n_taxonomy')) {

        // Localize terms.
        $parents = i18n_taxonomy_localize_terms($parents);
      }
      foreach (array_reverse($parents) as $parent) {
        $items[$entity_id][$delta]['parents'][$parent->tid] = $parent;
      }

      // Load term.
      $term_current = taxonomy_term_load($item[$field_column]);
      if (module_exists('i18n_taxonomy')) {

        // Localize current term.
        $term_current = i18n_taxonomy_localize_terms($term_current);
      }
      $items[$entity_id][$delta]['term'] = $term_current;
    }
  }
}

/**
 * Implements hook_field_formatter_view().
 */
function shs_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
  $elements = array();
  $settings = $display['settings'];
  if (empty($items) || $instance['widget']['type'] != 'taxonomy_shs') {
    return $elements;
  }
  switch ($display['type']) {
    case 'shs_default':
      foreach ($items as $delta => $item) {
        if (empty($item['term'])) {
          continue;
        }
        $list_items = array();

        // Add parent term names.
        foreach ($item['parents'] as $parent) {
          $list_items[] = array(
            'data' => $settings['linked'] ? l($parent->name, "taxonomy/term/{$parent->tid}") : $parent->name,
            'class' => array(
              'shs-parent',
            ),
          );
        }

        // Add name of selected term.
        $list_items[] = array(
          'data' => $settings['linked'] ? l($item['term']->name, "taxonomy/term/{$item['term']->tid}") : $item['term']->name,
          'class' => array(
            'shs-term-selected',
          ),
        );
        $elements[$delta] = array(
          '#items' => $list_items,
          '#theme' => 'item_list',
          '#attributes' => array(
            'class' => 'shs-hierarchy',
          ),
        );
      }

      // Add basic style.
      $elements['#attached']['css'][] = drupal_get_path('module', 'shs') . '/theme/shs.formatter.css';
      break;
  }
  return $elements;
}

/**
 * Function to get the list of children of a term.
 *
 * The structure is stored in the database cache as well as in drupal_static().
 * Cache has the following structure:
 * <code>
 *   [$parent] => array(),
 * </code>
 *
 * @param mixed $identifier
 *   Either a vocabulary ID or an array with the following keys:
 *   - field_name: Name of field which sends the request.
 * @param int $parent
 *   ID of parent term.
 * @param array $settings
 *   Additional settings (for example "language", etc.,).
 * @param bool $reset
 *   If TRUE, rebuild the cache for the given $vid and $parent.
 *
 * @return array
 *   List of child terms keyed by term id.
 */
function shs_term_get_children($identifier, $parent = 0, array $settings = array(), $reset = FALSE) {
  global $language;
  $langcode = $language->language;
  if (empty($settings['language']->language)) {
    $settings['language'] = $language;
  }
  else {
    $langcode = $settings['language']->language;
  }
  $vocabularies = array();
  $vocabulary_cache_key = NULL;
  if (is_numeric($identifier)) {
    $vocabulary_cache_key = $identifier;
    if (($vocabulary = taxonomy_vocabulary_load($identifier)) === FALSE) {
      watchdog('Simple hierarchical select', 'Unknown vocabulary with ID !vid used to get terms.', array(
        '!vid' => $identifier,
      ));
      return array();
    }
    $vocabularies[$vocabulary->machine_name] = $identifier;
  }
  elseif (is_array($identifier) && !empty($identifier['field_name'])) {
    $vocabulary_cache_key = $identifier['field_name'];
  }
  $terms =& drupal_static(__FUNCTION__, array());
  if ($reset || $vocabulary_cache_key && empty($terms[$vocabulary_cache_key][$langcode][$parent])) {

    // Initialize list.
    $terms[$vocabulary_cache_key][$langcode][$parent] = array();
    $cache_key = "shs:{$vocabulary_cache_key}";

    // Get cached values.
    $cache = cache_get($cache_key);
    if ($reset || !$cache || $cache->expire && time() > $cache->expire || empty($cache->data[$langcode][$parent])) {

      // Cache is empty or data has become outdated or the parent is not cached.
      if ($cache) {

        // Cache exists and is not yet expired but $parent is missing.
        $terms[$vocabulary_cache_key] = $cache->data;
      }
      if ($reset) {
        $terms[$vocabulary_cache_key][$langcode][$parent] = array();
      }
      if (!is_numeric($vocabulary_cache_key) && is_array($identifier)) {

        // Get list of vocabularies from field configuration.
        $field = field_info_field($identifier['field_name']);
        if (!empty($field['settings']['handler_settings']['target_bundles'])) {
          foreach ($field['settings']['handler_settings']['target_bundles'] as $vocabulary_name) {
            if (($vocabulary = taxonomy_vocabulary_machine_name_load($vocabulary_name)) !== FALSE) {
              $vocabularies[$vocabulary_name] = $vocabulary->vid;
            }
          }
        }
      }
      foreach ($vocabularies as $vid) {

        // Get term children (only first level).
        // Only load entities if i18n_taxonomy or entity_translation is
        // installed.
        $load_entities = module_exists('i18n_taxonomy') || module_exists('entity_translation');
        $tree = taxonomy_get_tree($vid, $parent, 1, $load_entities);
        foreach ($tree as $term) {
          $term_name = $term->name;
          if (module_exists('i18n_taxonomy')) {
            $term_name = i18n_taxonomy_term_name($term, $langcode);
          }
          elseif (module_exists('entity_translation')) {
            $term_name = entity_label('taxonomy_term', $term);
          }
          $terms[$vocabulary_cache_key][$langcode][$parent][$term->tid] = $term_name;
        }
      }

      // Set cached data.
      cache_set($cache_key, $terms[$vocabulary_cache_key], 'cache', CACHE_PERMANENT);
    }
    else {

      // Use cached data.
      $terms[$vocabulary_cache_key] = $cache->data;
    }
  }

  // Allow other module to modify the list of terms.
  $alter_options = array(
    'vid' => $vocabulary_cache_key,
    'parent' => $parent,
    'settings' => $settings,
  );
  drupal_alter('shs_term_get_children', $terms, $alter_options);
  return empty($terms[$vocabulary_cache_key][$langcode][$parent]) ? array() : $terms[$vocabulary_cache_key][$langcode][$parent];
}

/**
 * JSON callback to get the list of children of a term.
 *
 * @param int $vid
 *   ID of vocabulary the term is associated to.
 * @param array $parent
 *   List of parent terms.
 * @param array $settings
 *   Additional settings (for example "display node count").
 * @param string $field
 *   Name of field requesting the term list (DOM element name).
 *
 * @return array
 *   Associative list of child terms.
 *
 * @see shs_term_get_children()
 */
function shs_json_term_get_children($vid, array $parent = array(), array $settings = array(), $field = NULL) {
  $scope = $result = array();
  foreach ($parent as $tid) {
    $scope[] = shs_term_get_children($vid, $tid, $settings);
    if (shs_add_term_access($vid, $tid, $field)) {
      $result[] = array(
        'vid' => $vid,
      );
    }
  }

  // Rewrite result set to preserve original sort of terms through JSON request.
  foreach ($scope as $terms) {
    foreach ($terms as $tid => $label) {
      $result[] = array(
        'tid' => $tid,
        'label' => $label,
        'has_children' => shs_term_has_children($tid, $vid),
      );
    }
  }
  return $result;
}

/**
 * Check if a term has any children.
 *
 * Copied from taxonomy_get_children() but just checks existence instead of
 * loading terms.
 */
function shs_term_has_children($tid, $vid) {
  $query = db_select('taxonomy_term_data', 't');
  $query
    ->addExpression(1);
  $query
    ->join('taxonomy_term_hierarchy', 'h', 'h.tid = t.tid');
  $query
    ->condition('h.parent', $tid);
  if ($vid) {
    $query
      ->condition('t.vid', $vid);
  }
  $query
    ->range(0, 1);
  return (bool) $query
    ->execute()
    ->fetchField();
}

/**
 * Return access to create new terms.
 *
 * @param int $vid
 *   ID of vocabulary to create the term in.
 * @param int $parent
 *   ID of parent term (0 for top level).
 * @param string $field
 *   Name of field requesting the term list (DOM element name).
 * @param object $account
 *   The user to check access for.
 *
 * @return bool
 *   TRUE or FALSE based on if user can create a term.
 */
function shs_add_term_access($vid, $parent = NULL, $field = NULL, $account = NULL) {
  global $user;
  if (!$account) {
    $account = $user;
  }
  $access = module_invoke_all('shs_add_term_access', $vid, $parent, $field, $account);
  return !in_array(FALSE, $access, TRUE) && (user_access('edit terms in ' . $vid, $account) || in_array(TRUE, $access));
}

/**
 * Adds a term with ajax.
 *
 * @param string $token
 *   Custom validation token.
 * @param int $vid
 *   ID of vocabulary to create the term in.
 * @param int $parent
 *   ID of parent term (0 for top level).
 * @param string $term_name
 *   Name of new term.
 * @param string $field
 *   Name of field requesting the term list (DOM element name).
 *
 * @return mixed
 *   Array with tid and name or FALSE on error.
 */
function shs_json_term_add($token, $vid, $parent, $term_name, $field = NULL) {
  global $language_content;
  if (empty($token)) {
    return FALSE;
  }
  if (!is_numeric($vid)) {
    return FALSE;
  }
  if (!shs_add_term_access($vid, $parent, $field)) {

    // Sorry, but this user may not add a term to this vocabulary.
    return FALSE;
  }
  $term = (object) array(
    'vid' => $vid,
    'parent' => $parent,
    'name' => str_replace('&amp;', '&', filter_xss($term_name)),
    'language' => $language_content->language,
  );

  // Save term.
  $status = taxonomy_term_save($term);

  // Return term object or FALSE (in case of errors).
  return $status == SAVED_NEW ? array(
    'tid' => $term->tid,
    'name' => $term->name,
  ) : FALSE;
}

/**
 * Implements hook_hook_taxonomy_term_insert().
 */
function shs_taxonomy_term_insert($term) {
  if (empty($term->parent)) {
    return;
  }

  // Clear shs cache for current vocabulary.
  cache_clear_all("shs:{$term->vid}", 'cache');
}

/**
 * Implements hook_hook_taxonomy_term_update().
 */
function shs_taxonomy_term_update($term) {
  if (empty($term->parent)) {
    return;
  }

  // Clear shs cache for current vocabulary.
  cache_clear_all("shs:{$term->vid}", 'cache');
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function shs_form_taxonomy_overview_terms_alter(&$form, &$form_state, $form_id) {
  $form['#submit'][] = 'shs_form_taxonomy_overview_terms_submit';
}

/**
 * Implements hook_hook_taxonomy_term_delete().
 */
function shs_form_taxonomy_overview_terms_submit(&$form, &$form_state) {
  if (empty($form_state['complete form']['#vocabulary']->vid)) {
    return;
  }

  // Clear shs cache for current vocabulary.
  cache_clear_all("shs:{$form_state['complete form']['#vocabulary']->vid}", 'cache');
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function shs_form_taxonomy_form_term_alter(&$form, &$form_state, $form_id) {
  if (isset($form_state['confirm_delete']) && isset($form_state['values']['vid'])) {

    // Add custom submit handler to update cache.
    array_unshift($form['#submit'], 'shs_form_taxonomy_form_term_submit');
  }
}

/**
 * Submit callback for term form.
 */
function shs_form_taxonomy_form_term_submit(&$form, &$form_state) {

  // Clear shs cache for current vocabulary.
  cache_clear_all("shs:{$form_state['term']->vid}", 'cache');
}

/**
 * Helper function to get all instances of widgets with type "taxonomy_shs".
 *
 * @param string $entity_type
 *   Name of entity type.
 * @param string $bundle
 *   Name of bundle (optional).
 *
 * @return array
 *   List of instances keyed by field name.
 */
function _shs_get_instances($entity_type, $bundle = NULL) {
  $instances = array();
  $field_instances = field_info_instances($entity_type, $bundle);

  // Get all field instances with widget type "shs_taxonomy".
  if (empty($bundle)) {
    foreach ($field_instances as $bundle_name => $bundle_instances) {
      foreach ($bundle_instances as $instance) {
        if ($instance['widget']['type'] == 'taxonomy_shs') {
          $instances[$bundle_name][$instance['field_name']] = $instance;
        }
      }
    }
  }
  else {
    foreach ($field_instances as $instance) {
      if ($instance['widget']['type'] == 'taxonomy_shs') {
        $instances[$instance['field_name']] = $instance;
      }
    }
  }
  return $instances;
}

/**
 * Helper function to create a pseudo hash needed for javascript settings.
 *
 * @param int $length
 *   Lenght of string to return.
 *
 * @return string
 *   Random string.
 *
 * @see DrupalTestCase::randomName()
 */
function _shs_create_hash($length = 8) {
  $values = array_merge(range(65, 90), range(97, 122), range(48, 57));
  $max = count($values) - 1;
  $hash = chr(mt_rand(97, 122));
  for ($i = 1; $i < $length; $i++) {
    $hash .= chr($values[mt_rand(0, $max)]);
  }
  return $hash;
}

/**
 * Helper function to validate the vocabulary identifier coming from JSON.
 *
 * @param mixed $identifier
 *   Either a vocabulary ID or an array with the following keys:
 *   - field_name: Name of field which sends the request.
 *
 * @return bool
 *   TRUE if validation passes, otherwise FALSE.
 */
function shs_json_validation_vocabulary_identifier($identifier) {
  return TRUE;
}

/**
 * Helper function to get the vocabularies used in entityreference views.
 *
 * @param string $view_name
 *   Name of view.
 * @param string $display_id
 *   Name of display.
 *
 *   return string[]
 *   List of vocabulary identifiers.
 */
function _shs_entityreference_views_get_vocabularies($view_name, $display_id) {
  $view = views_get_view($view_name);
  if (empty($view)) {

    // Failed to load view.
    return array();
  }
  $filters = $view
    ->get_items('filter', $display_id);
  $vocabularies = array();
  foreach ($filters as $key => $filter) {
    if ('taxonomy_vocabulary' !== $filter['table'] || 'machine_name' !== $key) {
      continue;
    }
    $vocabularies = array_keys($filter['value']) + $vocabularies;
  }
  return $vocabularies;
}

Functions

Namesort descending Description
shs_add_term_access Return access to create new terms.
shs_conditional_fields_states_handlers_alter Implements hook_conditional_fields_states_handlers_alter().
shs_conditional_fields_states_handler_shs States handler for simple hierarchical selects.
shs_entityreference_field_settings_process Additional processing function for the entityreference field settings form.
shs_field_formatter_info Implements hook_field_formatter_info().
shs_field_formatter_prepare_view Implements hook_field_formatter_prepare_view().
shs_field_formatter_settings_form Implements hook_field_formatter_settings_form().
shs_field_formatter_settings_summary Implements hook_field_formatter_settings_summary().
shs_field_formatter_view Implements hook_field_formatter_view().
shs_field_views_data_alter Implements hook_field_views_data_alter().
shs_field_widget_afterbuild Afterbuild callback for widgets of type "taxonomy_shs".
shs_field_widget_error Implements hook_field_widget_error().
shs_field_widget_form Implements hook_field_widget_form().
shs_field_widget_info Implements hook_field_widget_info().
shs_field_widget_settings_form Implements hook_field_widget_settings_form().
shs_field_widget_validate Validation handler for widgets of type "taxonomy_shs".
shs_form_field_ui_field_edit_form_alter Implements hook_form_FORM_ID_alter().
shs_form_field_ui_field_settings_form_alter Implements hook_form_FORM_ID_alter().
shs_form_taxonomy_form_term_alter Implements hook_form_FORM_ID_alter().
shs_form_taxonomy_form_term_submit Submit callback for term form.
shs_form_taxonomy_overview_terms_alter Implements hook_form_FORM_ID_alter().
shs_form_taxonomy_overview_terms_submit Implements hook_hook_taxonomy_term_delete().
shs_js Implements hook_js().
shs_json Menu callback to get data in JSON format.
shs_json_callbacks Get a list of supported JSON callbacks.
shs_json_term_add Adds a term with ajax.
shs_json_term_get_children JSON callback to get the list of children of a term.
shs_json_validation_vocabulary_identifier Helper function to validate the vocabulary identifier coming from JSON.
shs_json_valid_token Wrapper around drupal_valid_token().
shs_menu Implements hook_menu().
shs_taxonomy_term_insert Implements hook_hook_taxonomy_term_insert().
shs_taxonomy_term_update Implements hook_hook_taxonomy_term_update().
shs_term_get_children Function to get the list of children of a term.
shs_term_has_children Check if a term has any children.
shs_views_data_alter Implements hook_views_data_alter().
_shs_create_hash Helper function to create a pseudo hash needed for javascript settings.
_shs_entityreference_views_get_vocabularies Helper function to get the vocabularies used in entityreference views.
_shs_get_instances Helper function to get all instances of widgets with type "taxonomy_shs".
_shs_json_callback_get_arguments Helper function to get the (validated) arguments for a JSON callback.