You are here

hierarchical_select.module in Hierarchical Select 5

This module defines the "hierarchical_select" form element, which is a greatly enhanced way for letting the user select an option in a hierarchy. Out of the box, this module supports the taxonomy and content_taxonomy modules, but that automatically includes the forum module. It also converts any hierarchical taxonomy exposed filters in any View to a hierarchical select.

Any module that uses a select form element, of which the options are ordered hierarchically, can take advantage of this new form element. Especially when there's a deep hierarchy, or when there are a lot of options in each sublevel, this form element greatly simplifies the finding of the right setting for the user. Note that due to the nature of this custom form item, it's currently impossible to select nothing. So by design, it is a required form item!

File

hierarchical_select.module
View source
<?php

/**
 * @file
 * This module defines the "hierarchical_select" form element, which is a
 * greatly enhanced way for letting the user select an option in a hierarchy.
 * Out of the box, this module supports the taxonomy and content_taxonomy
 * modules, but that automatically includes the forum module. It also converts
 * any hierarchical taxonomy exposed filters in any View to a hierarchical
 * select.
 *
 * Any module that uses a select form element, of which the options are
 * ordered hierarchically, can take advantage of this new form element.
 * Especially when there's a deep hierarchy, or when there are a lot of
 * options in each sublevel, this form element greatly simplifies the finding
 * of the right setting for the user. Note that due to the nature of this
 * custom form item, it's currently impossible to select nothing. So by
 * design, it is a required form item!
 */

// Enable default support for some modules, if they are enabled.
if (module_exists('taxonomy')) {
  require_once drupal_get_path('module', 'hierarchical_select') . '/modules/taxonomy.inc';
}
if (module_exists('content_taxonomy')) {
  require_once drupal_get_path('module', 'hierarchical_select') . '/modules/content_taxonomy.inc';
}

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

// Drupal core hooks.

/**
 * Implementation of hook_menu().
 */
function hierarchical_select_menu($may_cache) {
  if (!$maycache) {
    $items[] = array(
      'path' => 'hierarchical_select_ahah',
      'callback' => 'hierarchical_select_ahah',
      'access' => TRUE,
      // TODO: Check if this safe.
      'type' => MENU_CALLBACK,
    );
  }
  return $items;
}

/**
 * Implementation of hook_form_alter().
 */
function hierarchical_select_form_alter($form_id, &$form) {
  foreach (module_implements('hierarchical_select_form_alter') as $module) {
    $function = $module . '_hierarchical_select_form_alter';
    $function($form_id, $form);
  }
}

/**
 * Implementation of hook_elements().
 */
function hierarchical_select_elements() {
  $type['hierarchical_select'] = array(
    '#input' => TRUE,
    '#process' => array(
      'hierarchical_select_process' => array(),
    ),
    '#hierarchical_select_settings' => array(),
    '#default_value' => -1,
  );
  return $type;
}

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

// Menu callbacks.

/**
 * Menu callback; AHAH callback: generates and outputs the appropriate HTML.
 */
function hierarchical_select_ahah() {
  $params = $_POST;
  unset($params['module'], $params['hsid'], $params['selection'], $params['required']);
  $required = strcasecmp($_POST['required'], 'TRUE') == 0;
  print _hierarchical_select_render($_POST['hsid'], $_POST['module'], $_POST['selection'], FALSE, TRUE, $required, $params);
  exit;
}

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

// Forms API callbacks.

/**
 * Hierarchical select form element processing function.
 */
function hierarchical_select_process($element) {
  static $hsid;

  // Render a hierarchical select as a normal select, it's the JavaScript that
  // will turn it into a hierarchical select.
  $element['#type'] = 'select';
  if (!isset($hsid)) {
    $hsid = 0;
    $url = base_path();
    $url .= variable_get('clean_url', 0) ? '' : '?q=';
    $url .= 'hierarchical_select_ahah';

    // Add the CSS and JS, set the URL that should be used by all hierarchical
    // selects.
    drupal_add_css(drupal_get_path('module', 'hierarchical_select') . '/hierarchical_select.css');
    jquery_interface_add();
    drupal_add_js(drupal_get_path('module', 'hierarchical_select') . '/hierarchical_select.js');
    drupal_add_js(array(
      'hierarchical_select' => array(
        'url' => $url,
      ),
    ), 'setting');
  }
  else {
    $hsid++;
  }

  // Pass some settings for this hierarchical select.
  $module = $element['#hierarchical_select_settings']['module'];
  $params = $element['#hierarchical_select_settings']['params'];
  $lineage = $element['#hierarchical_select_settings']['lineage'];

  // When the #value property is empty, we're rendering this form (and thus
  // the form element) for the first time. When it's no longer empty, this
  // means that the validation failed and that we must keep the option that
  // was selected by the user.
  if (is_array($element['#value']) && empty($element['#value'])) {
    $selection = is_array($element['#default_value']) ? $element['#default_value'][0] : $element['#default_value'];
  }
  else {
    $selection = is_array($element['#value']) ? $element['#value'][0] : $element['#value'];
  }
  $initial = _hierarchical_select_render($hsid, $module, $selection, FALSE, TRUE, (bool) $element['#required'], $params);
  drupal_add_js(array(
    'hierarchical_select' => array(
      'settings' => array(
        $hsid => array(
          'initial' => $initial,
          'module' => $module,
          'required' => (bool) $element['#required'],
          'lineage' => (bool) $lineage,
          'params' => $params,
        ),
      ),
    ),
  ), 'setting');

  // If the lineage property is set to TRUE, make the select a multiple select.
  if ($lineage) {
    $element['#multiple'] = TRUE;
  }

  // Set the unique class.
  $element['#attributes']['class'] .= " hierarchical-select-{$hsid} hierarchical-select";
  return $element;
}

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

// Private functions.

/**
 * Render the hierarchical select.
 */
function _hierarchical_select_render($hsid, $module, $selection, $store_lineage = FALSE, $enforce_deepest = TRUE, $required = FALSE, $params = array()) {

  // DEBUG
  //  $params['vid'] = 1;
  //  $selection = 8
  if (!$selection) {
    $selection = $store_lineage ? array() : -1;
  }
  if (!module_invoke($module, 'hierarchical_select_valid_item', $selection, $params)) {
    $selection = -1;
  }

  // Build the hierarchy.
  $hierarchy = new StdClass();
  if (!$store_lineage) {
    $depth = 0;

    // Get the root level.
    $hierarchy->levels[$depth] = module_invoke($module, 'hierarchical_select_root_level', $params);
    $depth++;

    // Build the lineage
    $hierarchy->lineage = module_invoke($module, 'hierarchical_select_lineage', $selection);
    if ($enforce_deepest) {
      $hierarchy->lineage = _hierarchial_select_enforce_deepest_selection($hierarchy->lineage, $hierarchy->levels[0], $module, $params);
    }

    // Build the rest of the hierarchy, based on the lineage.
    while ($depth < count($hierarchy->lineage)) {
      $hierarchy->levels[$depth] = module_invoke($module, 'hierarchical_select_children', $hierarchy->lineage[$depth - 1], $params);
      $depth++;
    }
  }
  else {

    // todo
    $hierarchy->lineage = $selection;

    // $selection should be an array of selected stuff now.
  }

  // DEBUG
  //  dpr($hierarchy);
  //  print _hierarchical_select_render_selects($hsid, $hierarchy);
  //  exit;
  // END DEBUG
  return _hierarchical_select_render_selects($hsid, $hierarchy);
}

/**
 * Helper function to update the lineage of the hierarchy to ensure that the
 * user selects an item in the deepest level of the hierarchy.
 *
 * @param $lineage
 *   The lineage up to the deepest selection the user has made so far.
 * @param $root_level
 *   The options in the root level.
 * @param $module
 *   The module that should be used for HS hooks.
 * @param $params
 *   The params that should be passed to HS hooks.
 * @return
 *   The updated lineage.
 */
function _hierarchial_select_enforce_deepest_selection($lineage, $root_level, $module, $params) {
  $deepest_selection = end($lineage);

  // If no default is selected, thus the lineage is still empty, select the
  // first option of the root level by default.
  if (count($lineage) == 0) {
    $first_option = reset(array_keys($root_level));
    $lineage[] = $first_option;
    $deepest_selection = $first_option;
  }

  // Use the deepest selection as the first parent. Then apply this algorithm:
  // 1) get the parent's children, stop if no children
  // 2) choose the first child as the option that is selected by default, by
  //    adding it to the lineage of the hierarchy
  // 3) make this child the parent, go to step 1.
  $parent = $deepest_selection;
  $children = module_invoke($module, 'hierarchical_select_children', $parent, $params);
  while (count($children)) {
    $first_child = reset(array_keys($children));
    $lineage[] = $first_child;
    $parent = $first_child;
    $children = module_invoke($module, 'hierarchical_select_children', $parent, $params);
  }
  return $lineage;
}

/**
 * Render the HTML (the selects) for the given hierarchy..
 *
 * @param $hsid
 *   The hierarchical select id.
 * @param $hierarchy
 *   A hierarchy object.
 * @return
 *   The rendered HTML.
 */
function _hierarchical_select_render_selects($hsid, $hierarchy) {
  $output = '';
  for ($depth = 0; $depth < count($hierarchy->lineage); $depth++) {
    $output .= '<select id="hierarchical-select-' . $hsid . '-level-' . $depth . '" class="form-select hierarchical-select hierarchical-select-' . $hsid . '-hierarchical-select">';
    foreach ($hierarchy->levels[$depth] as $value => $label) {
      if ($value == $hierarchy->lineage[$depth]) {
        $output .= '<option selected="selected" value="' . $value . '">' . check_plain($label) . '</option>';
      }
      else {
        $output .= '<option value="' . $value . '">' . check_plain($label) . '</option>';
      }
    }
    $output .= '</select>';
  }
  return $output;
}

/**
 * Helper function that adds the JS to reposition the exposed filters of a
 * View just once.
 */
function _hierarchical_select_views_exposed_filters_reposition() {
  static $js_added;
  if (!isset($js_added)) {
    drupal_add_js(drupal_get_path('module', 'hierarchical_select') . '/modules/views.js', 'module');
  }
}

Functions

Namesort descending Description
hierarchical_select_ahah Menu callback; AHAH callback: generates and outputs the appropriate HTML.
hierarchical_select_elements Implementation of hook_elements().
hierarchical_select_form_alter Implementation of hook_form_alter().
hierarchical_select_menu Implementation of hook_menu().
hierarchical_select_process Hierarchical select form element processing function.
_hierarchial_select_enforce_deepest_selection Helper function to update the lineage of the hierarchy to ensure that the user selects an item in the deepest level of the hierarchy.
_hierarchical_select_render Render the hierarchical select.
_hierarchical_select_render_selects Render the HTML (the selects) for the given hierarchy..
_hierarchical_select_views_exposed_filters_reposition Helper function that adds the JS to reposition the exposed filters of a View just once.