You are here

hs_taxonomy.module in Hierarchical Select 7.3

Same filename and directory in other branches
  1. 5.3 modules/hs_taxonomy.module
  2. 6.3 modules/hs_taxonomy.module

Implementation of the Hierarchical Select API for the Taxonomy module.

File

modules/hs_taxonomy.module
View source
<?php

/**
 * @file
 * Implementation of the Hierarchical Select API for the Taxonomy module.
 */

/**
 * Implements hook_theme().
 */
function hs_taxonomy_theme() {
  return array(
    'hs_taxonomy_formatter_lineage' => array(
      'variables' => array(
        'lineage' => array(),
      ),
    ),
  );
}

/**
 * Implements hook_form_FORMID_alter().
 *
 * Alter the Hierarchical Select admin settings form to add a checkbox to
 * disable Hierarchical Select for taxonomy term edit forms.
 */
function hs_taxonomy_form_hierarchical_select_admin_settings_alter(&$form, &$form_state) {
  $form['taxonomy_override_selector'] = array(
    '#type' => 'checkbox',
    '#title' => t('Enable for taxonomy term edit forms'),
    '#description' => t('If this is checked then the "Relations > Parent terms" field on taxonomy term edit pages will use hierarchical select.'),
    '#default_value' => variable_get('taxonomy_override_selector', FALSE),
  );
}

/**
 * Implements hook_form_FORMID_alter().
 *
 * Alter the widget type form; dynamically add the Hierarchical Select
 * Configuration form when it is needed.
 */
function hs_taxonomy_form_field_ui_widget_type_form_alter(&$form, &$form_state) {
  form_load_include($form_state, 'inc', 'hierarchical_select', 'includes/common');

  // Alter the widget type select: configure #ajax so that we can respond to
  // changes in its value: whenever it is set to "taxonomy_hs", we add the HS
  // config UI.
  $form['basic']['widget_type']['#ajax'] = array(
    'event' => 'change',
    'callback' => 'hs_taxonomy_field_ui_widget_settings_ajax',
    'wrapper' => 'hs-config-replace',
    'method' => 'replace',
  );
  $current_widget_type = isset($form_state['input']['widget_type']) ? $form_state['input']['widget_type'] : $form_state['build_info']['args'][0]['widget']['type'];
  if ($current_widget_type == 'taxonomy_hs') {
    $field = field_info_field($form['#field_name']);
    if (!empty($field['settings']['allowed_values'][0]['vocabulary'])) {
      $vocabulary = taxonomy_vocabulary_machine_name_load($field['settings']['allowed_values'][0]['vocabulary']);
    }
    $vid = isset($vocabulary->vid) ? $vocabulary->vid : NULL;
    $save_lineage = isset($vocabulary->hierarchy) ? (int) ($vocabulary->hierarchy == 2) : 0;
    $instance = field_info_instance($form['#entity_type'], $form['#field_name'], $form['#bundle']);

    // Add the Hierarchical Select config form.
    $module = 'hs_taxonomy';
    $params = array(
      'vid' => $vid,
      'exclude_tid' => NULL,
      'root_term' => NULL,
      'allowed_max_depth' => 0,
    );
    $config_id = hs_taxonomy_get_config_id($form['#field_name']);
    $defaults = array(
      // Enable the save_lineage setting by default if the multiple parents
      // vocabulary option is enabled.
      'save_lineage' => $save_lineage,
      'editability' => array(
        'max_levels' => _hs_taxonomy_hierarchical_select_get_depth($vid),
      ),
    );
    $strings = array(
      'hierarchy' => t('taxonomy_vocabulary'),
      'hierarchies' => t('vocabularies'),
      'item' => t('term'),
      'items' => t('terms'),
      'item_type' => t('term type'),
      'entity' => t('node'),
      'entities' => t('nodes'),
    );
    $max_hierarchy_depth = _hs_taxonomy_hierarchical_select_get_depth($vid);
    $preview_is_required = $instance['required'] == 1;
    $form['hs'] = hierarchical_select_common_config_form($module, $params, $config_id, $defaults, $strings, $max_hierarchy_depth, $preview_is_required);
    if (!module_exists('taxonomy_entity_index')) {

      // Only allow person to select nodes.
      $form['hs']['entity_count']['settings']['entity_types'] = array(
        'node' => $form['hs']['entity_count']['settings']['entity_types']['node'],
      );
      $form['hs']['entity_count']['settings']['#collapsed'] = FALSE;
      $form['hs']['entity_count']['settings']['entity_types']['node']['#collapsed'] = FALSE;
      $form['hs']['entity_count']['#description'] = '<p>' . t('You can extend this functionality to other entities if you install !taxonomy_entity_index.', array(
        '!taxonomy_entity_index' => l('Taxonomy entity index', 'https://www.drupal.org/project/taxonomy_entity_index'),
      )) . '</p>';
    }

    // Make the config form AJAX-updateable.
    $form['hs'] += array(
      '#prefix' => '<div id="hs-config-replace">',
      '#suffix' => '</div>',
    );

    // Add the submit handler for the Hierarchical Select config form. Make
    // sure it is executed first.
    $form['#hs_common_config_form_parents'] = array(
      'hs',
    );
    array_unshift($form['#submit'], 'hierarchical_select_common_config_form_submit');

    // Add a submit handler for HS Taxonomy that will update the field
    // settings when necessary.
    // @see hs_taxonomy_field_settings_submit() for details.
    $form['#submit'][] = 'hs_taxonomy_field_settings_submit';
  }
  else {
    $form['hs'] = array(
      '#prefix' => '<div id="hs-config-replace">',
      '#suffix' => '</div>',
    );
  }
}

/**
 * Submit callback; updates the field settings (i.e. sets the cardinality of
 * the field to unlimited) whenever either the dropbox or "save lineage" is
 * enabled.
 */
function hs_taxonomy_field_settings_submit(&$form, &$form_state) {
  $field = field_info_field($form['#field_name']);
  $config = hierarchical_select_common_config_get(hs_taxonomy_get_config_id($form['#field_name']));
  if ($config['dropbox']['status'] || $config['save_lineage']) {
    $field = field_info_field($form['#field_name']);
    $field['cardinality'] = -1;

    // -1 = unlimited
    field_update_field($field);
    drupal_set_message(t("Updated this field's cardinality to unlimited."));
  }
}

/**
 * Implements hook_form_FORMID_alter().
 *
 * Alter the field settings form; dynamically disable the "cardinality" (or
 * "Number of values" in the UI) setting on the form when either the dropbox
 * or "save lineage" is enabled.
 */
function hs_taxonomy_form_field_ui_field_edit_form_alter(&$form, &$form_state) {
  if (isset($form['#field']['type']) && $form['#field']['type'] === 'taxonomy_term_reference' && $form['#instance']['widget']['type'] == 'taxonomy_hs') {
    require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'hierarchical_select') . '/includes/common.inc';
    $config = hierarchical_select_common_config_get(hs_taxonomy_get_config_id($form['#field']['field_name']));
    if ($config['dropbox']['status'] || $config['save_lineage']) {
      $form['field']['cardinality']['#disabled'] = TRUE;
      $form['field']['cardinality']['#description'] .= ' <strong>' . t('This setting is now managed by the Hierarchical Select configuration.') . '</strong>';
    }
  }
}
function hs_taxonomy_form_taxonomy_form_term_alter(&$form, &$form_state) {

  // Don't alter the form when taxonomy_override_selector is not TRUE (or 1).
  if (!variable_get('taxonomy_override_selector', FALSE)) {
    return;
  }

  // Don't alter the form when it's in confirmation mode.
  if (isset($form_state['confirm_delete']) || isset($form_state['confirm_parents'])) {
    return;
  }

  // Build an appropriate config.
  $vocabulary = $form['#vocabulary'];
  $vid = $vocabulary->vid;
  module_load_include('inc', 'hierarchical_select', 'includes/common');
  $config = array(
    'module' => 'hs_taxonomy',
    'params' => array(
      'vid' => $vid,
      'exclude_tid' => isset($form['#term']['tid']) ? $form['#term']['tid'] : NULL,
      'root_term' => TRUE,
    ),
    'enforce_deepest' => 0,
    'save_lineage' => 0,
    'level_labels' => array(
      'status' => FALSE,
      'labels' => array(),
    ),
    'dropbox' => array(
      'status' => variable_get('hs_taxonomy_enable_dropbox_on_term_form', 0),
      'limit' => 0,
    ),
    'editability' => array(
      'status' => 0,
    ),
    'entity_count' => array(
      'enable' => 0,
      'require_entity' => 0,
      'settings' => array(
        'count_children' => 0,
        'entity_types' => array(),
      ),
    ),
    'render_flat_select' => 0,
  );

  // Use Hierarchical Select for selecting the parent term(s).
  $parent_tid = array_keys(taxonomy_get_parents($form['#term']['tid']));
  $parent = !empty($parent_tid) ? $parent_tid : array(
    0,
  );
  $form['relations']['parent'] = array(
    '#type' => 'hierarchical_select',
    '#title' => t('Parents'),
    '#required' => TRUE,
    '#default_value' => $parent,
    '#config' => $config,
  );
  $form['relations']['parent']['#config']['dropbox']['title'] = t('All parent terms');
}

/**
 * Implements hook_field_delete().
 *
 * This enables us to delete HS configs when fields are deleted.
 */
function hs_taxonomy_field_delete($entity_type, $entity, $field, $instance, $langcode, &$items) {
  require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'hierarchical_select') . '/includes/common.inc';
  hierarchical_select_common_config_del(hs_taxonomy_get_config_id($field['field_name']));
}

//----------------------------------------------------------------------------

// FAPI callbacks.

/**
 * AJAX callback; field UI widget settings form.
 */
function hs_taxonomy_field_ui_widget_settings_ajax($form, &$form_state) {
  return $form['hs'];
}

//----------------------------------------------------------------------------

// Field API widget hooks.

/**
 * Implements hook_field_widget_info().
 */
function hs_taxonomy_field_widget_info() {
  return array(
    'taxonomy_hs' => array(
      'label' => t('Hierarchical Select'),
      'field types' => array(
        'taxonomy_term_reference',
      ),
      'settings' => array(),
      // All set in hs_taxonomy_field_widget_form().
      'behaviors' => array(
        // TODO: figure out how to map the "dropbox" behavior to Field API's
        // "multiple values" system.
        'multiple values' => FIELD_BEHAVIOR_CUSTOM,
      ),
    ),
  );
}

/**
 * Implements hook_field_widget_form().
 */
function hs_taxonomy_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
  require_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'hierarchical_select') . '/includes/common.inc';
  if (!empty($field['settings']['allowed_values'][0]['vocabulary'])) {
    $vocabulary = taxonomy_vocabulary_machine_name_load($field['settings']['allowed_values'][0]['vocabulary']);
  }

  // Build an array of existing term IDs.
  $tids = array();
  foreach ($items as $delta => $item) {
    if (!empty($item['tid']) && $item['tid'] != 'autocreate') {
      $tids[] = $item['tid'];
    }
  }
  $element += array(
    '#type' => 'hierarchical_select',
    '#config' => array(
      'module' => 'hs_taxonomy',
      'params' => array(
        'vid' => isset($vocabulary->vid) ? (int) $vocabulary->vid : NULL,
        'exclude_tid' => NULL,
        'root_term' => isset($field['settings']['allowed_values'][0]['parent']) ? (int) $field['settings']['allowed_values'][0]['parent'] : NULL,
      ),
    ),
    '#default_value' => $tids,
  );
  hierarchical_select_common_config_apply($element, hs_taxonomy_get_config_id($field['field_name']));

  // Set allowed max depth to value saved in config if available.
  $element['#config']['params']['allowed_max_depth'] = isset($element['#config']['allowed_max_depth']) ? (int) $element['#config']['allowed_max_depth'] : NULL;

  // Append another #process callback that transforms #return_value to the
  // format that Field API/Taxonomy Field expects.
  // However, HS' default #process callback has not yet been set, since this
  // typically happens automatically during FAPI processing. To ensure the
  // order is right, we already set HS' own #process callback here explicitly.
  $element_info = element_info('hierarchical_select');
  $element['#process'] = array_merge($element_info['#process'], array(
    'hs_taxonomy_widget_process',
  ));
  return $element;
}

/**
 * Implements hook_field_widget_settings_form().
 */
function hs_taxonomy_field_widget_settings_form($field, $instance) {

  // This poorly integrates with the Field UI. Hence we alter the
  // field_ui_widget_type_form, to provide a more appropriate integration.
  // @see hs_taxonomy_form_field_ui_widget_type_form_alter.
  $form = array();
  return $form;
}

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

/**
 * #process callback that runs after HS' #process callback, to transform
 * #return_value to the format that Field API/Taxonomy Field expects.
 */
function hs_taxonomy_widget_process($element, &$form_state, $complete_form) {
  $tids = $element['#return_value'];

  // If #return_value is array(NULL), then nothing was selected!
  if (count($tids) == 1 && $tids[0] === NULL) {
    $element['#return_value'] = array();
    return $element;
  }
  $items = array();
  foreach ($tids as $tid) {
    $items[] = array(
      'tid' => $tid,
    );
  }
  $element['#return_value'] = $items;
  return $element;
}

//----------------------------------------------------------------------------

// Field API formatter hooks.

/**
 * Implements hook_field_formatter_info().
 */
function hs_taxonomy_field_formatter_info() {
  return array(
    'hs_taxonomy_term_reference_hierarchical_text' => array(
      'label' => t('Hierarchical text'),
      'field types' => array(
        'taxonomy_term_reference',
      ),
    ),
    'hs_taxonomy_term_reference_hierarchical_links' => array(
      'label' => t('Hierarchical links'),
      'field types' => array(
        'taxonomy_term_reference',
      ),
    ),
    'hs_taxonomy_term_reference_hierarchical_links_last_text' => array(
      'label' => t('Hierarchical links last text'),
      'field types' => array(
        'taxonomy_term_reference',
      ),
    ),
    'hs_taxonomy_term_reference_hierarchical_text_last_link' => array(
      'label' => t('Hierarchical text last link'),
      'field types' => array(
        'taxonomy_term_reference',
      ),
    ),
    'hs_taxonomy_term_reference_last_link_only' => array(
      'label' => t('Last link only'),
      'field types' => array(
        'taxonomy_term_reference',
      ),
    ),
    'hs_taxonomy_term_reference_last_text_only' => array(
      'label' => t('Last text only'),
      'field types' => array(
        'taxonomy_term_reference',
      ),
    ),
  );
}

/**
 * Implements hook_field_formatter_prepare_view().
 */
function hs_taxonomy_field_formatter_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items, $displays) {

  // Extract required field information.
  $vocabulary = taxonomy_vocabulary_machine_name_load($field['settings']['allowed_values'][0]['vocabulary']);
  $vid = $vocabulary->vid;

  // Get the config for this field.
  module_load_include('inc', 'hierarchical_select', 'includes/common');
  $config_id = hs_taxonomy_get_config_id($field['field_name']);
  $config = hierarchical_select_common_config_get($config_id);
  $config += array(
    'module' => 'hs_taxonomy',
    'params' => array(
      'vid' => $vid,
    ),
  );

  // Collect every possible term attached to any of the fieldable entities.
  // Copied from taxonomy_field_formatter_prepare_view().
  foreach ($entities as $id => $entity) {
    $selection = array();
    foreach ($items[$id] as $delta => $item) {

      // Force the array key to prevent duplicates.
      if ($item['tid'] != 'autocreate') {
        $key = in_array($item['tid'], $selection) ? array_search($item['tid'], $selection) : $delta;
        $selection[$key] = $item['tid'];
      }
    }

    // Generate a dropbox out of the selection. This will automatically
    // calculate all lineages for us.
    $dropbox = _hierarchical_select_dropbox_generate($config, $selection);

    // Store additional information in each item that's required for
    // Hierarchical Select's custom formatters that are compatible with the
    // save_lineage functionality.
    if (!empty($dropbox->lineages)) {
      foreach (array_keys($dropbox->lineages) as $lineage) {
        foreach ($dropbox->lineages[$lineage] as $level => $details) {
          $tid = $details['value'];

          // Look up where this term (tid) is stored in the items array.
          $key = array_search($tid, $selection);

          // Store the additional information. One term can occur in multiple
          // lineages: when Taxonomy's "multiple parents" functionality is
          // being used.
          $items[$id][$key]['hs_lineages'][] = array(
            'lineage' => $lineage,
            'level' => $level,
            'label' => $details['label'],
            'tid' => $tid,
          );
        }
      }
    }
  }
}

/**
 * Implements hook_field_formatter_view().
 */
function hs_taxonomy_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {

  // Extract required field information.
  $vocabulary = taxonomy_vocabulary_machine_name_load($field['settings']['allowed_values'][0]['vocabulary']);

  // Extract the lineage information from the items (this was added by
  // hs_taxonomy_field_formatter_prepare_view()).
  $lineages = array();
  foreach ($items as $delta => $item) {
    if (!empty($item['hs_lineages'])) {
      $metadata = $item['hs_lineages'];
      for ($i = 0; $i < count($metadata); $i++) {
        $term = new StdClass();
        $term->tid = $metadata[$i]['tid'];
        $term->vid = $vocabulary->vid;
        $term->vocabulary_machine_name = $vocabulary->machine_name;
        $term->name = $metadata[$i]['label'];
        $lineages[$metadata[$i]['lineage']][$metadata[$i]['level']] = $term;
      }
    }
  }

  // Actual formatting.
  $element = array();
  switch ($display['type']) {
    case 'hs_taxonomy_term_reference_hierarchical_text':
      for ($l = 0; $l < count($lineages); $l++) {
        $element[$l]['#theme'] = 'hs_taxonomy_formatter_lineage';
        for ($level = 0; $level < count($lineages[$l]); $level++) {
          $term = $lineages[$l][$level];
          $element[$l]['#lineage'][$level] = array(
            '#markup' => $term->name,
          );
        }
      }
      break;
    case 'hs_taxonomy_term_reference_hierarchical_links':
      for ($l = 0; $l < count($lineages); $l++) {
        $element[$l]['#theme'] = 'hs_taxonomy_formatter_lineage';
        for ($level = 0; $level < count($lineages[$l]); $level++) {
          $term = $lineages[$l][$level];
          $uri = entity_uri('taxonomy_term', $term);
          $uri['options']['html'] = TRUE;
          $element[$l]['#lineage'][$level] = array(
            '#type' => 'link',
            '#title' => $term->name,
            '#href' => $uri['path'],
            '#options' => $uri['options'],
          );
        }
      }
      break;
    case 'hs_taxonomy_term_reference_hierarchical_links_last_text':
      for ($l = 0; $l < count($lineages); $l++) {
        $element[$l]['#theme'] = 'hs_taxonomy_formatter_lineage';
        for ($level = 0; $level < count($lineages[$l]) - 1; $level++) {
          $term = $lineages[$l][$level];
          $uri = entity_uri('taxonomy_term', $term);
          $element[$l]['#lineage'][$level] = array(
            '#type' => 'link',
            '#title' => $term->name,
            '#href' => $uri['path'],
            '#options' => $uri['options'],
          );
        }
        if (count($lineages[$l]) > 0) {
          $level = count($lineages[$l]) - 1;
          $term = $lineages[$l][$level];
          $element[$l]['#lineage'][$level] = array(
            '#markup' => $term->name,
          );
        }
      }
      break;
    case 'hs_taxonomy_term_reference_hierarchical_text_last_link':
      for ($l = 0; $l < count($lineages); $l++) {
        $element[$l]['#theme'] = 'hs_taxonomy_formatter_lineage';
        for ($level = 0; $level < count($lineages[$l]) - 1; $level++) {
          $term = $lineages[$l][$level];
          $element[$l]['#lineage'][$level] = array(
            '#markup' => $term->name,
          );
        }
        if (count($lineages[$l]) > 0) {
          $level = count($lineages[$l]) - 1;
          $term = $lineages[$l][$level];
          $uri = entity_uri('taxonomy_term', $term);
          $element[$l]['#lineage'][$level] = array(
            '#type' => 'link',
            '#title' => $term->name,
            '#href' => $uri['path'],
            '#options' => $uri['options'],
          );
        }
      }
      break;
    case 'hs_taxonomy_term_reference_last_text_only':
      for ($l = 0; $l < count($lineages); $l++) {
        $element[$l]['#theme'] = 'hs_taxonomy_formatter_lineage';
        if (count($lineages[$l]) > 0) {
          $level = count($lineages[$l]) - 1;
          $term = $lineages[$l][$level];
          $element[$l]['#lineage'][0] = array(
            '#markup' => $term->name,
          );
        }
      }
      break;
    case 'hs_taxonomy_term_reference_last_link_only':
      for ($l = 0; $l < count($lineages); $l++) {
        $element[$l]['#theme'] = 'hs_taxonomy_formatter_lineage';
        if (count($lineages[$l]) > 0) {
          $level = count($lineages[$l]) - 1;
          $term = $lineages[$l][$level];
          $uri = entity_uri('taxonomy_term', $term);
          $element[$l]['#lineage'][0] = array(
            '#type' => 'link',
            '#title' => $term->name,
            '#href' => $uri['path'],
            '#options' => $uri['options'],
          );
        }
      }
      break;
  }
  if (!empty($element)) {
    $element['#attached']['css'][] = drupal_get_path('module', 'hierarchical_select') . '/hierarchical_select.css';
  }
  return $element;
}

//----------------------------------------------------------------------------

// Hierarchical Select hooks.

/**
 * Implementation of hook_hierarchical_select_params().
 */
function hs_taxonomy_hierarchical_select_params() {
  $params = array(
    'vid',
    'exclude_tid',
    // Allows a term to be excluded (necessary for the taxonomy_form_term form).
    'root_term',
    // Displays a fake "<root>" term in the root level (necessary for the
    // taxonomy_form-term form).
    'entity_count_for_node_type',
    // Restrict the entity count to a specific node type.
    'allowed_max_depth',
  );
  return $params;
}

/**
 * Implementation of hook_hierarchical_select_root_level().
 */
function hs_taxonomy_hierarchical_select_root_level($params) {
  if (!isset($params['vid'])) {
    return array();
  }

  // TODO: support multiple parents, i.e. support "save lineage".
  $vocabulary = taxonomy_vocabulary_load($params['vid']);
  $terms = _hs_taxonomy_hierarchical_select_get_tree($params['vid'], 0, -1, 1);

  // If the root_term parameter is enabled, then prepend a fake "<root>" term.
  if (isset($params['root_term']) && $params['root_term'] === TRUE) {
    $root_term = new StdClass();
    $root_term->tid = 0;
    $root_term->name = '<' . t('root') . '>';
    $terms = array_merge(array(
      $root_term,
    ), $terms);
  }

  // Unset the term that's being excluded, if it is among the terms.
  if (isset($params['exclude_tid'])) {
    foreach ($terms as $key => $term) {
      if ($term->tid == $params['exclude_tid']) {
        unset($terms[$key]);
      }
    }
  }

  // If the Term Permissions module is installed, honor its settings.
  if (module_exists('term_permissions')) {
    global $user;
    foreach ($terms as $key => $term) {
      if (!term_permissions_allowed($term->tid, $user)) {
        unset($terms[$key]);
      }
    }
  }
  return _hs_taxonomy_hierarchical_select_terms_to_options($terms);
}

/**
 * Implementation of hook_hierarchical_select_children().
 */
function hs_taxonomy_hierarchical_select_children($parent, $params) {
  static $tree;
  $depth = $params['allowed_max_depth'];
  $vid = $params['vid'];
  if (isset($params['root_term']) && $params['root_term'] && $parent == 0 || !is_numeric($parent)) {
    return array();
  }
  $terms = taxonomy_get_tree($params['vid'], $parent, 1);

  // Unset the term that's being excluded, if it is among the children.
  if (isset($params['exclude_tid'])) {
    unset($terms[$params['exclude_tid']]);
  }

  // If the Term Permissions module is installed, honor its settings.
  if (module_exists('term_permissions')) {
    global $user;
    foreach ($terms as $key => $term) {
      if (!term_permissions_allowed($term->tid, $user)) {
        unset($terms[$key]);
      }
    }
  }

  // Keep a static cache of the entire tree, this allows us to quickly look up
  // if a term is not too deep – because if it's too deep, we don't want to
  // return any children.
  if (!isset($tree[$vid])) {
    $raw_tree = _hs_taxonomy_hierarchical_select_get_tree($vid);
    foreach ($raw_tree as $term) {
      $tree[$vid][$term->tid] = $term->depth;
    }
  }
  $terms = $depth > 0 && $tree[$vid][$parent] + 1 >= $depth ? array() : _hs_taxonomy_hierarchical_select_get_tree($vid, $parent, -1, 1);
  return _hs_taxonomy_hierarchical_select_terms_to_options($terms);
}

/**
 * Implementation of hook_hierarchical_select_lineage().
 */
function hs_taxonomy_hierarchical_select_lineage($item, $params) {
  $lineage = array();
  if (isset($params['root_term']) && $params['root_term'] && $item == 0) {
    return array(
      0,
    );
  }
  $terms = array_reverse(hs_taxonomy_get_parents_all($item));
  foreach ($terms as $term) {
    $lineage[] = $term->tid;
  }
  return $lineage;
}

/**
 * Alternative version of taxonomy_get_parents_all(): instead of using all
 * parents of a term (i.e. when multiple parents are being used), only the
 * first is kept.
 */
function hs_taxonomy_get_parents_all($tid) {
  $parents = array();
  if ($term = taxonomy_term_load($tid)) {
    $parents[] = $term;
    $n = 0;
    while ($parent = taxonomy_get_parents($parents[$n]->tid)) {
      $parents = array_merge($parents, array(
        reset($parent),
      ));
      $n++;
    }
  }
  return $parents;
}

/**
 * Implements hook_hierarchical_select_valid_item().
 */
function hs_taxonomy_hierarchical_select_valid_item($item, $params) {
  if (isset($params['root_term']) && $params['root_term'] && $item == 0) {
    return TRUE;
  }
  if (!is_numeric($item) || $item < 1 || isset($params['exclude_tid']) && $item == $params['exclude_tid']) {
    return FALSE;
  }
  $term = taxonomy_term_load($item);
  if (!$term) {
    return FALSE;
  }

  // If the Term Permissions module is installed, honor its settings.
  if (module_exists('term_permissions')) {
    global $user;
    if (!term_permissions_allowed($term->tid, $user)) {
      return FALSE;
    }
  }
  if (!isset($params['root_term'])) {
    $params['root_term'] = NULL;
  }
  if (!isset($params['allowed_max_depth'])) {
    $params['allowed_max_depth'] = 0;
  }
  return $term->vid == $params['vid'] && _hs_taxonomy_term_within_allowed_depth($term->tid, $term->vid, $params['root_term'], $params['allowed_max_depth']);
}

/**
 * Implements hook_hierarchical_select_item_get_label().
 */
function hs_taxonomy_hierarchical_select_item_get_label($item, $params) {
  static $labels = array();
  if (!isset($labels[$item])) {
    if ($item === 0 && isset($params['root_term']) && $params['root_term'] === TRUE) {
      $term = new StdClass();
      $term->name = '<' . t('root') . '>';
    }
    else {
      $term = taxonomy_term_load($item);

      // Try to translate the label if the i18n module is available.
      if (module_exists('i18n_taxonomy')) {
        $term->name = i18n_taxonomy_term_name($term);
      }
    }
    $labels[$item] = $term->name;
  }
  return $labels[$item];
}

/**
 * Implements hook_hierarchical_select_create_item().
 *
 * TRICKY: No depth check is necessary in this function because HS's internal
 *         validation prevents an invalid parent.
 */
function hs_taxonomy_hierarchical_select_create_item($label, $parent, $params) {
  $term = new StdClass();
  $term->vid = $params['vid'];
  $term->name = html_entity_decode($label, ENT_QUOTES);
  $term->description = '';
  $term->parent = $parent;
  $status = taxonomy_term_save($term);
  if ($status !== FALSE) {

    // Reset the cached tree.
    _hs_taxonomy_hierarchical_select_get_tree($params['vid'], 0, -1, 1, TRUE);

    // Retrieve the tid.
    $children = _hs_taxonomy_hierarchical_select_get_tree($params['vid'], $parent, 1);
    foreach ($children as $term) {
      if ($term->name == $label) {
        return $term->tid;
      }
    }
  }
  else {
    return FALSE;
  }
}

/**
 * Implementation of hook_hierarchical_select_entity_count().
 */
function hs_taxonomy_hierarchical_select_entity_count($item, $params) {
  $num_entities = 0;
  $selected_bundles = $params['entity_count']['settings']['entity_types'];
  $count_children = $params['entity_count']['settings']['count_children'];

  // Maybe this needs some more caching and value-updates on entity_save()/
  // _update()/delete().
  if (empty($num_entities)) {
    $index_table = 'taxonomy_index';
    if (module_exists('taxonomy_entity_index')) {
      $index_table = 'taxonomy_entity_index';
    }

    // Count entities associated to this term.
    $query = db_select($index_table, 'ti');
    $query
      ->fields('ti');
    $query
      ->condition('ti.tid', $item);
    if (module_exists('taxonomy_entity_index')) {
      _hs_taxonomy_add_entity_bundles_condition_to_query($query, $selected_bundles);
    }
    $result = $query
      ->execute();
    $num_entities = $result
      ->rowCount();
    if ($count_children) {
      $tids = array();
      $tree = taxonomy_get_tree($params['vid'], $item);
      foreach ($tree as $child_term) {
        $tids[] = $child_term->tid;
      }
      if (count($tids)) {

        // Count entities associated to child terms.
        $query = db_select($index_table, 'ti');
        $query
          ->fields('ti');
        $query
          ->condition('ti.tid', $tids, 'IN');
        if (module_exists('taxonomy_entity_index')) {
          _hs_taxonomy_add_entity_bundles_condition_to_query($query, $selected_bundles);
        }
        $result = $query
          ->execute();
        $num_entities += $result
          ->rowCount();
      }
    }
  }
  return $num_entities;
}

/**
 * Implementation of hook_hierarchical_select_implementation_info().
 */
function hs_taxonomy_hierarchical_select_implementation_info() {
  return array(
    'hierarchy type' => t('Taxonomy'),
    'entity type' => t('Node'),
  );
}

/**
 * Implementation of hook_hierarchical_select_config_info().
 */
function hs_taxonomy_hierarchical_select_config_info() {
  static $config_info;
  if (!isset($config_info)) {
    $config_info = array();
    $fields = field_info_fields();
    foreach ($fields as $field_name => $field) {
      foreach ($field['bundles'] as $entity_type => $bundles) {
        $bundle_links = array();
        foreach ($bundles as $bundle) {
          $instance = field_info_instance($entity_type, $field_name, $bundle);
          if ($instance['widget']['type'] == 'taxonomy_hs') {
            $bundles_info = field_info_bundles($entity_type);
            $bundle_links[] = l($bundles_info[$bundle]['label'], $bundles_info[$bundle]['admin']['real path']);
            $entity_info = entity_get_info($entity_type);
            $machine_name = $field['settings']['allowed_values'][0]['vocabulary'];
            $vocabulary = taxonomy_vocabulary_machine_name_load($machine_name);
            $config_id = hs_taxonomy_get_config_id($field_name);
            $config_info[$config_id] = array(
              'config_id' => $config_id,
              'hierarchy type' => t('Taxonomy'),
              'hierarchy' => t('Vocabulary') . ': ' . l(t($vocabulary->name), "admin/structure/taxonomy/{$machine_name}") . '<br /><small>' . t('Field label') . ': ' . $instance['label'] . '<br />' . t('Field machine name') . ': ' . $field_name . '</small>',
              'entity type' => $entity_info['label'],
              'bundle' => implode('<br />', $bundle_links),
              'context type' => '',
              'context' => '',
              'edit link' => isset($bundles_info[$bundle]['admin']['real path']) ? $bundles_info[$bundle]['admin']['real path'] . "/fields/{$field_name}/widget-type" : $bundles_info[$bundle]['admin']['path'] . "/fields/{$field_name}/widget-type",
            );
          }
        }
      }
    }
  }
  return $config_info;
}

//----------------------------------------------------------------------------

// Token hooks.

/**
 * Implementation of hook_token_values().
 */

/*
// TODO: port this to D7.
function hs_taxonomy_token_values($type, $object = NULL, $options = array()) {
  static $hs_vids;
  static $all_vids;

  $separator = variable_get('hs_taxonomy_separator', variable_get('pathauto_separator', '-'));

  $values = array();
  switch ($type) {
    case 'node':
      $node = $object;

      // Default values.
      $values['save-lineage-termpath'] = $values['save-lineage-termpath-raw'] = '';

      // If $node->taxonomy doesn't exist, these tokens cannot be created!
      if (!is_object($node) || !isset($node->taxonomy) || !is_array($node->taxonomy)) {
        return $values;
      }

      // Find out which vocabularies are using Hierarchical Select.
      if (!isset($hs_vids)) {
        $hs_vids = array();
        // TODO Please convert this statement to the D7 database API syntax.
        $result = db_query("SELECT SUBSTRING(name, 30, 3) AS vid FROM {variable} WHERE name LIKE 'taxonomy_hierarchical_select_%' AND value LIKE 'i:1\;';");
        while ($o = db_fetch_object($result)) {
          $hs_vids[] = $o->vid;
        }
      }

      // Get a list of all existent vids, so we can generate an empty token
      // when a token is requested for a vocabulary that's not associated with
      // the current content type.
      if (!isset($all_vids)) {
        $all_vids = array();
        $result = db_query("SELECT vid FROM {taxonomy_vocabulary}");
        while ($row = db_fetch_object($result)) {
          $all_vids[] = $row->vid;
        }
      }

      // Generate the per-vid "save-lineage-termpath" tokens.
      foreach ($all_vids as $vid) {
        $terms = array();
        if (in_array($vid, $hs_vids) && isset($node->taxonomy[$vid])) {
          $selection = $node->taxonomy[$vid];
          $terms = _hs_taxonomy_token_termpath_for_vid($selection, $vid);
        }

        $terms_raw = $terms;
        $terms = array_map('check_plain', $terms);
        $values["save-lineage-termpath:$vid"] = !empty($options['pathauto']) ? $terms : implode($separator, $terms);
        $values["save-lineage-termpath-raw:$vid"] = !empty($options['pathauto']) ? $terms_raw : implode($separator, $terms_raw);
      }

      // We use the terms of the first vocabulary that uses Hierarchical
      // Select for the default "save-lineage-termpath" tokens.
      $vids = array_intersect(array_keys($node->taxonomy), $hs_vids);
      if (!empty($vids)) {
        $vid = $vids[0];
        $values['save-lineage-termpath'] = $values["save-lineage-termpath:$vid"];
        $values['save-lineage-termpath-raw'] = $values["save-lineage-termpath-raw:$vid"];
      }
      break;
  }

  return $values;
}
*/

/**
 * Implementation of hook_token_list().
 */

/*
// TODO: port this to D7.
function hs_taxonomy_token_list($type = 'all') {
  if ($type == 'node' || $type == 'all') {
    $tokens['node']['save-lineage-termpath'] = t('Only use when you have enabled the "save lineage" setting of Hierarchical Select. Will show the term\'s parent terms separated by /.');
    $tokens['node']['save-lineage-termpath-raw'] = t('As [save-linage-termpath]. WARNING - raw user input.');

    $tokens['node']['save-lineage-termpath:vid'] = t('Only has output when terms are present for the vocabulary with the specified vid. Only use when you have enabled the "save lineage" setting of Hierarchical Select. Will show the term\'s parent terms separated by /.');
    $tokens['node']['save-lineage-termpath-raw:vid'] = t('Only has output when terms are present for the vocabulary with the specified vid. As [save-linage-termpath]. WARNING - raw user input.');

    return $tokens;
  }
}
*/

/**
 * Helper function for hs_taxonomy_token_values().
 */
function _hs_taxonomy_token_termpath_for_vid($selection, $vid) {
  $terms = array();
  $selection = is_array($selection) ? $selection : array(
    $selection,
  );

  // Generate the part we'll need of the Hierarchical Select configuration.
  $config = array(
    'module' => 'hs_taxonomy',
    'save_lineage' => 1,
    'params' => array(
      'vid' => $vid,
      'exclude_tid' => NULL,
      'root_term' => NULL,
    ),
  );

  // Validate all items in the selection, if any.
  if (!empty($selection)) {
    foreach ($selection as $key => $item) {
      $valid = module_invoke($config['module'], 'hierarchical_select_valid_item', $selection[$key], $config['params']);
      if (!$valid) {
        unset($selection[$key]);
      }
    }
  }

  // Generate a dropbox out of the selection. This will automatically
  // calculate all lineages for us.
  // If the selection is empty, then the tokens will be as well.
  if (!empty($selection)) {
    $dropbox = _hierarchical_select_dropbox_generate($config, $selection);

    // If no lineages could be generated, these tokens cannot be created!
    if (empty($dropbox->lineages)) {
      return $terms;
    }

    // We pick the first lineage.
    $lineage = $dropbox->lineages[0];

    // Finally, we build the tokens.
    foreach ($lineage as $item) {
      $terms[] = $item['label'];
    }
  }
  return $terms;
}

//----------------------------------------------------------------------------

// Theme functions.

/**
 * Format a lineage for one of HS Taxonomy's custom Term reference formatters.
 */
function theme_hs_taxonomy_formatter_lineage($variables) {
  $output = '';
  $lineage = $variables['lineage'];
  $separator = theme('hierarchical_select_item_separator');

  // Render each item within a lineage.
  $items = array();
  foreach ($lineage as $level => $item) {
    $line = '<span class="lineage-item lineage-item-level-' . $level . '">';
    $line .= drupal_render($item);
    $line .= '</span>';
    $items[] = $line;
  }
  $output .= implode($separator, $items);
  return $output;
}

//----------------------------------------------------------------------------

// Private functions.

/**
 * Drupal core's taxonomy_get_tree() doesn't allow us to reset the cached
 * trees, which obviously causes problems when you create new items between
 * two calls to it.
 */
function _hs_taxonomy_hierarchical_select_get_tree($vid, $parent = 0, $depth = -1, $max_depth = NULL, $reset = FALSE) {
  static $children, $parents, $terms;
  if ($reset) {
    $children = $parents = $terms = array();
  }
  $tree = array();
  if (!is_array($parent)) {
    $parent = array(
      $parent,
    );
  }
  $max_depth = is_null($max_depth) ? 99999999 : $max_depth;
  $depth++;

  // We cache trees, so it's not CPU-intensive to call get_tree() on a term
  // and its children, too.
  if ($max_depth <= $depth) {
    return $tree;
  }

  // Prepare queue for the "IN ( .. )" part of query.
  $queue = array();
  foreach ($parent as $single_parent) {

    // Queue branch for processing if it's not cached yet.
    if (!isset($children[$vid][$single_parent])) {
      $queue[] = $single_parent;

      // Use an empty array to distinguish between a stub (without children)
      // term and a branch that is not loaded yet.
      $children[$vid][$single_parent] = array();
    }
  }
  if (!empty($queue)) {
    $query = db_select('taxonomy_term_data', 't');
    $query
      ->join('taxonomy_term_hierarchy', 'h', 'h.tid = t.tid');
    $result = $query
      ->addTag('translatable')
      ->addTag('term_access')
      ->addTag('hs_taxonomy_tree')
      ->fields('t')
      ->fields('h', array(
      'parent',
    ))
      ->condition('t.vid', $vid)
      ->condition('parent', array_merge(array(
      $vid,
    ), $queue), 'IN')
      ->orderBy('t.weight')
      ->orderBy('t.name')
      ->execute();
    foreach ($result as $term) {
      $children[$vid][$term->parent][] = $term->tid;
      $parents[$vid][$term->tid][] = $term->parent;
      $terms[$vid][$term->tid] = $term;
    }
  }

  // Provide support for Title module. If Title module is enabled and this
  // vocabulary uses translated term names we want output those terms with their
  // translated version. Therefore a full taxonomy term entity load is required,
  // similar to taxonomy_get_tree().
  if (!empty($terms) && module_exists('title')) {
    $vocabulary = taxonomy_vocabulary_load($vid);
    if (title_field_replacement_enabled('taxonomy_term', $vocabulary->machine_name, 'name')) {
      $term_entities = taxonomy_term_load_multiple(array_keys($terms[$vid]));
    }
  }
  $next_parent = array();
  foreach ($parent as $single_parent) {
    foreach ($children[$vid][$single_parent] as $child) {
      $term = isset($term_entities[$child]) ? $term_entities[$child] : $terms[$vid][$child];
      $term = clone $term;
      $term->depth = $depth;

      // The "parent" attribute is not useful, as it would show one parent only.
      unset($term->parent);
      $term->parents = $parents[$vid][$child];
      $tree[] = $term;

      // Need more steps ?
      if ($max_depth > $depth + 1) {

        // Queue children for the next step down the tree. Do not process
        // children which we already know as stub ones.
        if (!isset($children[$vid][$child]) || !empty($children[$vid][$child])) {
          $next_parent[] = $child;
        }
      }
    }
  }
  if (!empty($next_parent)) {

    // Process multiple children together i.e. next level.
    $tree = array_merge($tree, _hs_taxonomy_hierarchical_select_get_tree($vid, $next_parent, $depth, $max_depth));
  }
  return isset($tree) ? $tree : array();
}

/**
 * Returns the configuration ID that would be used for the specified field.
 *
 * @param string $field_name
 *   The field machine name.
 *
 * @return string
 *   The config id for the provided field.
 */
function hs_taxonomy_get_config_id($field_name) {
  return "taxonomy-{$field_name}";
}

/**
 * Drupal core's taxonomy_term_count_nodes() is buggy. See
 * http://drupal.org/node/144969#comment-843000.
 */
function hs_taxonomy_term_count_nodes($tid, $type = 0) {
  static $count;
  $tids = array(
    $tid,
  );
  if ($term = taxonomy_term_load($tid)) {
    $tree = _hs_taxonomy_hierarchical_select_get_tree($term->vid, $tid);
    foreach ($tree as $descendant) {
      $tids[] = $descendant->tid;
    }
  }
  if (!isset($count[$type][$tid])) {
    $query = db_select('taxonomy_term_node', 't');
    $query
      ->join('node', 'n', 't.nid = n.nid');
    $query
      ->addExpression('COUNT(DISTINCT(n.nid))', 'count')
      ->condition('n.status', 1)
      ->condition('t,tid', $tids);
    if (!is_numeric($type)) {
      $query
        ->condition('n.type', $type);
    }
    $query
      ->addTag('hs_taxonomy_term_count_nodes');
    $query
      ->addTag('term_access');
    $result = $query
      ->execute();
    $count[$type][$tid] = $result
      ->fetchField();
  }
  return $count[$type][$tid];
}

/**
 * Transform an array of terms into an associative array of options, for use
 * in a select form item.
 *
 * @param $terms
 *  An array of term objects.
 * @return
 *  An associative array of options, keys are tids, values are term names.
 */
function _hs_taxonomy_hierarchical_select_terms_to_options($terms) {
  $options = array();
  $use_i18n = module_exists('i18n_taxonomy');
  foreach ($terms as $key => $term) {

    // Use the translated term when available!
    $options[$term->tid] = $use_i18n && isset($term->vid) ? i18n_taxonomy_term_name($term) : $term->name;
  }
  return $options;
}

/**
 * Get the depth of a vocabulary's tree.
 *
 * @param $vid
 *   A vocabulary id.
 * @return
 *   The depth of the vocabulary's tree.
 */
function _hs_taxonomy_hierarchical_select_get_depth($vid) {
  $depth = -99999;
  $tree = _hs_taxonomy_hierarchical_select_get_tree($vid);
  foreach ($tree as $term) {
    if ($term->depth > $depth) {
      $depth = $term->depth;
    }
  }
  return $depth;
}

/**
 * Helper function to add entity_type and bundles to count query in form of a 
 * and/or combination.
 *
 * @param object $query
 *  A db_select query object.
 * @param array $selected_bundles
 * An associative array of entities that contain bundles.
 */
function _hs_taxonomy_add_entity_bundles_condition_to_query(&$query, $selected_bundles) {
  $db_or = db_or();
  foreach ($selected_bundles as $entity_type => $bundles) {
    $db_and = db_and();
    $db_and
      ->condition('ti.entity_type', $entity_type);
    $db_and
      ->condition('ti.bundle', array_shift($bundles), 'IN');
    $db_or
      ->condition($db_and);
  }
  $query
    ->condition($db_or);
}

/**
 * Helper function to check if taxonomy term is within allowed depth.
 *
 * @param int $tid
 *   The term id.
 * @param int $vid
 *   The vocabulary id.
 * @param int $root_tid
 *   The term id of th4e parent taxonomy term.
 * @param int $allowed_depth
 *   The number of maxium taxonomy depth allowed.
 */
function _hs_taxonomy_term_within_allowed_depth($tid, $vid, $root_tid, $allowed_depth) {

  // If the allowed depth is zero, then every term is allowed!
  if ($allowed_depth == 0) {
    return TRUE;
  }

  // Otherwise, only allow terms that are within the allowed depth.
  static $valid_tids;
  if (!isset($valid_tids[$vid][$root_tid][$allowed_depth])) {
    $valid_tids[$vid][$root_tid][$allowed_depth] = array();
    $tree = _hs_taxonomy_hierarchical_select_get_tree($vid, $root_tid);
    foreach ($tree as $term) {
      if ($term->depth < $allowed_depth) {
        $valid_tids[$vid][$root_tid][$allowed_depth][] = $term->tid;
      }
    }
  }
  return in_array($tid, $valid_tids[$vid][$root_tid][$allowed_depth]);
}

Functions

Namesort descending Description
hs_taxonomy_field_delete Implements hook_field_delete().
hs_taxonomy_field_formatter_info Implements hook_field_formatter_info().
hs_taxonomy_field_formatter_prepare_view Implements hook_field_formatter_prepare_view().
hs_taxonomy_field_formatter_view Implements hook_field_formatter_view().
hs_taxonomy_field_settings_submit Submit callback; updates the field settings (i.e. sets the cardinality of the field to unlimited) whenever either the dropbox or "save lineage" is enabled.
hs_taxonomy_field_ui_widget_settings_ajax AJAX callback; field UI widget settings form.
hs_taxonomy_field_widget_error Implements hook_field_widget_error().
hs_taxonomy_field_widget_form Implements hook_field_widget_form().
hs_taxonomy_field_widget_info Implements hook_field_widget_info().
hs_taxonomy_field_widget_settings_form Implements hook_field_widget_settings_form().
hs_taxonomy_form_field_ui_field_edit_form_alter Implements hook_form_FORMID_alter().
hs_taxonomy_form_field_ui_widget_type_form_alter Implements hook_form_FORMID_alter().
hs_taxonomy_form_hierarchical_select_admin_settings_alter Implements hook_form_FORMID_alter().
hs_taxonomy_form_taxonomy_form_term_alter
hs_taxonomy_get_config_id Returns the configuration ID that would be used for the specified field.
hs_taxonomy_get_parents_all Alternative version of taxonomy_get_parents_all(): instead of using all parents of a term (i.e. when multiple parents are being used), only the first is kept.
hs_taxonomy_hierarchical_select_children Implementation of hook_hierarchical_select_children().
hs_taxonomy_hierarchical_select_config_info Implementation of hook_hierarchical_select_config_info().
hs_taxonomy_hierarchical_select_create_item Implements hook_hierarchical_select_create_item().
hs_taxonomy_hierarchical_select_entity_count Implementation of hook_hierarchical_select_entity_count().
hs_taxonomy_hierarchical_select_implementation_info Implementation of hook_hierarchical_select_implementation_info().
hs_taxonomy_hierarchical_select_item_get_label Implements hook_hierarchical_select_item_get_label().
hs_taxonomy_hierarchical_select_lineage Implementation of hook_hierarchical_select_lineage().
hs_taxonomy_hierarchical_select_params Implementation of hook_hierarchical_select_params().
hs_taxonomy_hierarchical_select_root_level Implementation of hook_hierarchical_select_root_level().
hs_taxonomy_hierarchical_select_valid_item Implements hook_hierarchical_select_valid_item().
hs_taxonomy_term_count_nodes Drupal core's taxonomy_term_count_nodes() is buggy. See http://drupal.org/node/144969#comment-843000.
hs_taxonomy_theme Implements hook_theme().
hs_taxonomy_widget_process #process callback that runs after HS' #process callback, to transform #return_value to the format that Field API/Taxonomy Field expects.
theme_hs_taxonomy_formatter_lineage Format a lineage for one of HS Taxonomy's custom Term reference formatters.
_hs_taxonomy_add_entity_bundles_condition_to_query Helper function to add entity_type and bundles to count query in form of a and/or combination.
_hs_taxonomy_hierarchical_select_get_depth Get the depth of a vocabulary's tree.
_hs_taxonomy_hierarchical_select_get_tree Drupal core's taxonomy_get_tree() doesn't allow us to reset the cached trees, which obviously causes problems when you create new items between two calls to it.
_hs_taxonomy_hierarchical_select_terms_to_options Transform an array of terms into an associative array of options, for use in a select form item.
_hs_taxonomy_term_within_allowed_depth Helper function to check if taxonomy term is within allowed depth.
_hs_taxonomy_token_termpath_for_vid Helper function for hs_taxonomy_token_values().