You are here

term_level.module in Term Level Field 7

Field type for referencing terms with a level to an entity.

File

term_level.module
View source
<?php

/**
 * @file
 * Field type for referencing terms with a level to an entity.
 */

// term_level_element form type
include_once 'term_level_element.inc';

// We currently only support vocabularies that have 3 (root, 1, 2) levels or
// less.
define('TERM_LEVEL_VOC_MAX_DEPTH', 2);

/**
 * Implements hook_field_info().
 */
function term_level_field_info() {
  return array(
    'term_level' => array(
      'label' => 'Term Level Field',
      'description' => t('This field references a term with a selectable level.'),
      'default_widget' => 'term_level_widget',
      'default_formatter' => 'term_level_default',
      'settings' => array(
        'voc' => '',
        'levels' => '',
      ),
      // Properties for Entity Metadata.
      'property_type' => 'field_item_term_level',
      'property_callbacks' => array(
        'term_level_entity_metadata_callback',
      ),
    ),
  );
}

/**
 * Entity metadata callback for term_level settings.
 */
function term_level_entity_metadata_callback(&$info, $entity_type, $field, $instance, $field_type) {
  $property =& $info[$entity_type]['bundles'][$instance['bundle']]['properties'][$field['field_name']];
  $property['getter callback'] = 'entity_metadata_field_verbatim_get';
  $property['setter callback'] = 'entity_metadata_field_verbatim_set';
  unset($property['query callback']);
  $property['property info'] = array(
    'term' => array(
      'type' => 'taxonomy_term',
      'label' => t('Taxonomy Term'),
      'getter callback' => 'term_level_entity_metadata_field_get_term',
    ),
    'level' => array(
      'type' => 'integer',
      'label' => t('Level'),
    ),
  );
  $property['options list'] = 'entity_metadata_field_options_list';
}

/**
 * Getter callback for term in term level field.
 */
function term_level_entity_metadata_field_get_term($item, array $options, $name, $type, $context) {
  return $item['tid'];
}

/**
 * Implements hook_options_list().
 */
function term_level_options_list($field) {
  $levels = _term_level_extract_levels($field['settings']['levels']);
  if (module_exists('i18n_field')) {
    $levels = i18n_string_translate(array(
      'field',
      $field['field_name'],
      '#levels',
    ), $levels);
  }
  return $levels;
}

/**
 * Implements hook_field_is_empty().
 */
function term_level_field_is_empty($item, $field) {
  if (!is_array($item) || empty($item['tid']) || !isset($item['level']) || $item['level'] === 'none') {
    return TRUE;
  }
  return FALSE;
}

/**
 * Implements hook_theme().
 */
function term_level_theme($existing, $type, $theme, $path) {
  return array(
    'term_level_element' => array(
      'render element' => 'element',
    ),
    'term_level_group' => array(
      'variables' => array(
        'levels' => array(),
        'level' => 0,
        'term_names' => array(),
      ),
    ),
  );
}

/**
 * Implements hook_field_formatter_info().
 */
function term_level_field_formatter_info() {
  return array(
    'term_level_default' => array(
      'label' => t('Terms with level'),
      'field types' => array(
        'term_level',
      ),
    ),
    'term_level_group' => array(
      'label' => t('Terms grouped by their level'),
      'field types' => array(
        'term_level',
      ),
    ),
  );
}

/**
 * Implements hook_field_formatter_view().
 */
function term_level_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
  drupal_add_css(drupal_get_path('module', 'term_level') . '/term_level.css');
  $levels = term_level_options_list($field);
  $term_levels = array();
  $level_groups = array();
  foreach ($items as $delta => $item) {
    $term_levels[$item['tid']] = $item['level'];
    $level_groups[$item['level']][] = $item['tid'];
  }
  $terms = taxonomy_term_load_multiple(array_keys($term_levels));
  $element = array();
  if ($display['type'] == 'term_level_default') {
    foreach ($term_levels as $tid => $level_key) {
      if (isset($terms[$tid])) {
        $term = $terms[$tid];
        $element[$tid] = array(
          '#markup' => check_plain($term->name) . ': ' . check_plain($levels[$level_key]),
        );
      }
    }
  }
  elseif ($display['type'] == 'term_level_group') {
    krsort($level_groups);
    foreach ($level_groups as $level_key => $tids) {
      $term_names = array();
      foreach ($tids as $tid) {
        if (isset($terms[$tid])) {
          $term = $terms[$tid];
          $term_names[] = check_plain($term->name);
        }
      }
      $element[$level_key] = array(
        '#theme' => 'term_level_group',
        '#levels' => $levels,
        '#level' => $level_key,
        '#term_names' => $term_names,
      );
    }
  }
  return $element;
}

/**
 * Implements hook_field_settings_form().
 */
function term_level_field_settings_form($field, $instance, $has_data) {
  $vocabularies = taxonomy_get_vocabularies();
  $options = array();
  foreach ($vocabularies as $vocabulary) {
    $options[$vocabulary->machine_name] = $vocabulary->name;
  }
  $form['voc'] = array(
    '#type' => 'select',
    '#title' => t('Vocabulary'),
    '#default_value' => $field['settings']['voc'],
    '#options' => $options,
    '#required' => TRUE,
    '#description' => t('The vocabulary which supplies the options for this field. The vocabulary structure either needs to be a flat list or a two-level hierarchy, where the terms get grouped by their parents in the first level.'),
    '#disabled' => $has_data,
  );
  $form['levels'] = array(
    '#type' => 'textarea',
    '#title' => t('Levels'),
    '#default_value' => $field['settings']['levels'],
    '#required' => TRUE,
    '#description' => t('Specify the term levels for this field. Enter one level per line, in the format level-key|level-label (level-key must be numeric).'),
    '#element_validate' => array(
      'term_level_field_settings_levels_validate',
    ),
    '#disabled' => $has_data,
  );
  return $form;
}

/**
 * Validates that level settings are correct.
 */
function term_level_field_settings_levels_validate($element, &$form_state) {
  $levels = _term_level_extract_levels($element['#value']);
  if (count($levels) == 0) {
    form_error($element, t('Please enter valid levels.'));
  }
  foreach ($levels as $key => $label) {
    if (!is_numeric($key)) {
      form_error($element, t('The level key must be numeric.'));
    }
  }
}

/**
 * Implements hook_i18n_string_list_field_alter().
 */
function term_level_i18n_string_list_field_alter(&$properties, $type, $object) {
  if ($type == "field" && $object['type'] == "term_level") {
    $levels = _term_level_extract_levels($object['settings']['levels']);
    foreach ($levels as $key => $level) {
      $properties['field'][$object['field_name']]['#levels'][$key] = array(
        'title' => t('Level %name', array(
          '%name' => $level,
        )),
        'string' => $level,
      );
    }
  }
}

/**
 * Implements hook_field_widget_info().
 */
function term_level_field_widget_info() {
  return array(
    'term_level_widget' => array(
      'label' => t('Term Level Widget'),
      'field types' => array(
        'term_level',
      ),
      'behaviors' => array(
        'multiple values' => FIELD_BEHAVIOR_CUSTOM,
      ),
      'settings' => array(
        'tag_cloud' => FALSE,
        'term_entries_limit_per_group' => FIELD_CARDINALITY_UNLIMITED,
        'validate_empty_terms' => FALSE,
      ),
    ),
  );
}

/**
 * Implements hook_widget_settings_form().
 */
function term_level_field_widget_settings_form($field, $instance) {
  $settings = $instance['widget']['settings'];
  $form['term_entries_limit_per_group'] = array(
    '#type' => 'select',
    '#title' => t('Number of terms per group'),
    '#options' => array(
      FIELD_CARDINALITY_UNLIMITED => t('Unlimited'),
    ) + drupal_map_assoc(range(0, 10)),
    '#default_value' => $settings['term_entries_limit_per_group'],
  );
  $form['tag_cloud'] = array(
    '#type' => 'checkbox',
    '#title' => t('Enable Tag Cloud'),
    '#default_value' => $settings['tag_cloud'],
    '#description' => t('If checked, each term group will have a tag cloud with addional terms not listed in the table.'),
  );
  $form['validate_empty_terms'] = array(
    '#type' => 'checkbox',
    '#title' => t('Validate empty terms'),
    '#default_value' => $settings['validate_empty_terms'],
    '#description' => t('If checked, terms with no selected level will throw a validation errror. This forces the user to either select a level or remove the term from the table group.'),
  );
  return $form;
}

/**
 * Implements hook_field_widget_form().
 */
function term_level_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
  $field_settings = $field['settings'];
  $widget_settings = $instance['widget']['settings'];
  $voc = taxonomy_vocabulary_machine_name_load($field_settings['voc']);
  $levels = term_level_options_list($field);
  $default_values = _term_level_extract_default_values($items);

  // Submitted values through AJAX.
  if (isset($form_state['values']['default_terms'])) {
    foreach ($form_state['values']['default_terms'] as $tid => $level) {
      $default_values[$tid] = $level;
    }
  }

  // Term added through AJAX.
  $added_term = 0;
  if (isset($form_state['values']['added_term']) && $form_state['values']['added_term']) {
    $added_term = $form_state['values']['added_term'];
  }
  $groups = _term_level_get_groups($voc->vid, $widget_settings['term_entries_limit_per_group'], $default_values, $added_term);
  $element['#element_validate'] = array(
    'term_level_widget_validate',
  );
  $element['#required'] = $instance['required'];
  $label = filter_xss_admin($instance['label']);
  $label = $instance['required'] ? $label . ' ' . theme('form_required_marker', array()) : $label;
  $element['label'] = array(
    '#markup' => '<label class="term-level-widget-label">' . $label . '</label>',
  );
  if (!empty($instance['description'])) {
    $element['description'] = array(
      '#markup' => '<div class="term-level-widget-description description">' . filter_xss_admin($instance['description'] . '</div>'),
    );
  }
  foreach ($groups['parents'] as $parent => $parent_name) {
    $element[] = array(
      '#type' => 'term_level',
      '#title' => $parent_name,
      '#terms' => isset($groups['terms'][$parent]) ? $groups['terms'][$parent] : array(),
      '#levels' => $levels,
      '#parent' => $parent,
      '#vid' => $voc->vid,
      '#tag_cloud_terms' => $widget_settings['tag_cloud'] && isset($groups['tag_cloud'][$parent]) ? $groups['tag_cloud'][$parent] : array(),
    );
  }
  return $element;
}

/**
 * Extracts values form term level field and sets the value for saving.
 */
function term_level_widget_validate($element, &$form_state) {
  $instance = field_info_instance($element['#entity_type'], $element['#field_name'], $element['#bundle']);
  $values = array();
  foreach (element_children($element) as $key) {
    if (isset($element[$key]['terms']) && is_array($element[$key]['terms'])) {
      foreach (element_children($element[$key]['terms']) as $tid) {
        $level = $element[$key]['terms'][$tid]['#value'];
        if (!is_null($level) && $level != "none") {
          $values[] = array(
            'tid' => $tid,
            'level' => $level,
          );
        }
        elseif (!empty($instance['widget']['settings']['validate_empty_terms']) && is_null($level)) {
          $term = $element[$key]['#terms'][$tid];
          form_error($element[$key]['terms'][$tid], t('%term has no level specified. Please select one or remove it from the table.', array(
            '%term' => $term->name,
          )));
        }
      }
    }
  }

  // Use own required validation here, as the default validation does not work
  // here (tag_cloud_term_options always exists).
  if ($element['#required'] && !count($values)) {
    form_error($element, t('!name field is required.', array(
      '!name' => $element['#title'],
    )));
  }
  form_set_value($element, $values, $form_state);
}

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

/**
 * Extracts levels (level-key => level-label) out of the field settings.
 *
 * Level-label are not yet sanitized.
 */
function _term_level_extract_levels($level_settings) {
  $levels = array();
  $list = explode("\n", $level_settings);
  $list = array_map('trim', $list);
  $list = array_filter($list, 'strlen');
  foreach ($list as $key => $value) {
    if (strpos($value, '|') !== FALSE) {
      list($level_key, $label) = explode('|', $value);
    }
    if (isset($level_key) && isset($level_key)) {
      $levels[$level_key] = $label;
    }
  }
  return $levels;
}

/**
 * Transforms the default values into a more usable structure.
 */
function _term_level_extract_default_values($items = array()) {
  $default_values = array();
  foreach ($items as $item) {
    $default_values[$item['tid']] = $item['level'];
  }
  return $default_values;
}

/**
 * Returns list of terms, group by their parents or voc.
 *
 * Terms have an row_weight value set, used for ordering the table rows,
 * correct ordering is important when rebuilding the form element:
 *  - default_values are on top (within default_values order by term order)
 *  - added_terms are always on the bottom (weight 9999)
 *  - the rest is in between (according to term order)
 *
 * @return An array containing following entries:
 *   - 'parents': Each entry represents its own term level table group.
 *   - 'terms': The terms that should be listed in the table, keyed by the
 *        table parent.
 *   - 'tag_cloud': An array keyed by the table parent that either contains the
 *        entry 'all' with all tag cloud terms, or 'groups' with an additional
 *        group level for taxonomies with 3 hierarchical levels.
 */
function _term_level_get_groups($vid, $term_entries_limit_per_group, $default_values = array(), $added_term = 0) {
  $groups = array();
  $groups['terms'] = array();
  $groups['parents'] = array();
  $groups['tag_cloud'] = array();
  $tree = taxonomy_get_tree($vid, 0, TERM_LEVEL_VOC_MAX_DEPTH + 1);

  // Check how many levels the vocabulary has. We currently only support
  // vocabularies that have 3 (root, 1, 2) levels or less.
  $max_depth = 0;
  foreach ($tree as $term) {
    if ($term->depth > $max_depth) {
      $max_depth = $term->depth;
      if ($max_depth == TERM_LEVEL_VOC_MAX_DEPTH) {
        break;
      }
    }
  }

  // Flat vocabulary, use the voc name for the table group.
  if ($max_depth == 0) {
    $voc = taxonomy_vocabulary_load($vid);
    $groups['parents'][0] = $voc->name;
  }
  $term_entries = 0;
  foreach ($tree as $term) {

    // Create the groups (only for hierarchical vocs).
    if ($max_depth != 0 && $term->depth == 0) {
      $groups['parents'][$term->tid] = $term->name;
      $term_entries = 0;
    }
    else {
      if ($max_depth != 1 && $term->depth == 1) {
        $top_parent = reset($term->parents);
        $groups['level_2'][$term->tid] = array(
          'term' => $term,
          'parent' => $top_parent,
        );
        $groups['tag_cloud'][$top_parent]['group_terms'][$term->tid] = $term;
      }
      elseif ($term->depth == $max_depth) {

        // Check if the term should be listed in the table:
        //  - either all terms are shown in table
        //  - or the limit isn't reached yet
        //  - or the current term is in the default values and doesn't have the
        //      level 'none'
        //  - or the current term has been added from the tag cloud
        if ($term_entries_limit_per_group == FIELD_CARDINALITY_UNLIMITED || $term_entries < $term_entries_limit_per_group || in_array($term->tid, array_keys($default_values)) || $term->tid === $added_term) {
          if (in_array($term->tid, array_keys($default_values))) {
            $term->level = $default_values[$term->tid];
            $term->row_weight = -1;
            if ($term->level == 'none') {
              _term_level_groups_add_to_tag_clould($groups, $term);
            }
          }
          if ($term->tid === $added_term) {
            $term->level = NULL;

            // List the new term at the bottom of the table.
            $term->row_weight = 9999;
          }
          if (!isset($term->row_weight)) {
            $term->row_weight = $term_entries;
          }
          _term_level_groups_add_to_table($groups, $term);
          $term_entries++;
        }
        else {
          _term_level_groups_add_to_tag_clould($groups, $term);
        }
      }
    }
  }
  return $groups;
}

/**
 * Helper function that adds a term to the table structure.
 */
function _term_level_groups_add_to_table(&$groups, $term) {
  $term_parent = end($term->parents);
  if (isset($groups['level_2'][$term_parent])) {
    $term_parent = $groups['level_2'][$term_parent]['parent'];
  }
  $groups['terms'][$term_parent][$term->tid] = $term;
}

/**
 * Helper function that adds a tag cloud term to the array structure.
 */
function _term_level_groups_add_to_tag_clould(&$groups, $term) {
  $term_parent = end($term->parents);
  if (isset($groups['level_2'][$term_parent])) {
    $top_term_parent = $groups['level_2'][$term_parent]['parent'];
    $groups['tag_cloud'][$top_term_parent]['groups'][$term_parent][$term->tid] = $term;
  }
  else {
    $groups['tag_cloud'][$term_parent]['all'][$term->tid] = $term;
  }
}

/**
 * Themes the term_level_group formatter (per level).
 */
function theme_term_level_group(&$variables) {
  if (isset($variables['levels'][$variables['level']]) && !empty($variables['term_names'])) {
    $markup = '<div class="term-level-field-label term-level-field-label-level-' . $variables['level'] . '">' . check_plain($variables['levels'][$variables['level']]) . ': </div>';
    $markup .= '<div class="term-level-field-items term-level-field-items-level-' . $variables['level'] . '">' . implode(', ', $variables['term_names']) . '</div>';
    return $markup;
  }
}

Functions

Namesort descending Description
term_level_entity_metadata_callback Entity metadata callback for term_level settings.
term_level_entity_metadata_field_get_term Getter callback for term in term level field.
term_level_field_formatter_info Implements hook_field_formatter_info().
term_level_field_formatter_view Implements hook_field_formatter_view().
term_level_field_info Implements hook_field_info().
term_level_field_is_empty Implements hook_field_is_empty().
term_level_field_settings_form Implements hook_field_settings_form().
term_level_field_settings_levels_validate Validates that level settings are correct.
term_level_field_widget_error Implements hook_field_widget_error().
term_level_field_widget_form Implements hook_field_widget_form().
term_level_field_widget_info Implements hook_field_widget_info().
term_level_field_widget_settings_form Implements hook_widget_settings_form().
term_level_i18n_string_list_field_alter Implements hook_i18n_string_list_field_alter().
term_level_options_list Implements hook_options_list().
term_level_theme Implements hook_theme().
term_level_widget_validate Extracts values form term level field and sets the value for saving.
theme_term_level_group Themes the term_level_group formatter (per level).
_term_level_extract_default_values Transforms the default values into a more usable structure.
_term_level_extract_levels Extracts levels (level-key => level-label) out of the field settings.
_term_level_get_groups Returns list of terms, group by their parents or voc.
_term_level_groups_add_to_table Helper function that adds a term to the table structure.
_term_level_groups_add_to_tag_clould Helper function that adds a tag cloud term to the array structure.

Constants