[ 'render element' => 'element', 'function' => 'theme_checkbox_tree', ], 'checkbox_tree_level' => [ 'render element' => 'element', 'function' => 'theme_checkbox_tree_level', ], 'checkbox_tree_item' => [ 'render element' => 'element', 'function' => 'theme_checkbox_tree_item', ], 'checkbox_tree_label' => [ 'render element' => 'element', 'function' => 'theme_checkbox_tree_label', ], 'term_tree_list' => [ 'render element' => 'element', 'function' => 'theme_term_tree_list', ], ]; } /** * Returns HTML for a checkbox_tree form element. */ function theme_checkbox_tree($variables) { $element = $variables['element']; $element['#children'] = drupal_render_children($element); $attributes = isset($element['#attributes']) ? $element['#attributes'] : []; if (isset($element['#id'])) { $attributes['id'] = $element['#id']; } $attributes['class'][] = 'term-reference-tree'; if (!empty($element['#required'])) { $attributes['class'][] = 'required'; } if (array_key_exists('#start_minimized', $element) && $element['#start_minimized']) { $attributes['class'][] = 'term-reference-tree-collapsed'; } if (array_key_exists('#track_list', $element) && $element['#track_list']) { $attributes['class'][] = 'term-reference-tree-track-list-shown'; } if (!empty($variables['element']['#select_parents'])) { $attributes['class'][] = 'term-reference-tree-select-parents'; } if ($variables['element']['#cascading_selection'] != \Drupal\term_reference_tree\Plugin\Field\FieldWidget\TermReferenceTree::CASCADING_SELECTION_NONE) { $attributes['class'][] = 'term-reference-tree-cascading-selection'; } if ($variables['element']['#cascading_selection'] == \Drupal\term_reference_tree\Plugin\Field\FieldWidget\TermReferenceTree::CASCADING_SELECTION_SELECT) { $attributes['class'][] = 'term-reference-tree-cascading-selection-mode-select'; } else { if ($variables['element']['#cascading_selection'] == \Drupal\term_reference_tree\Plugin\Field\FieldWidget\TermReferenceTree::CASCADING_SELECTION_DESELECT) { $attributes['class'][] = 'term-reference-tree-cascading-selection-mode-deselect'; } } if (!empty($element['#attributes']['class'])) { $attributes['class'] = array_merge($attributes['class'], $element['#attributes']['class']); } return '' . (!empty($element['#children']) ? $element['#children'] : '') . ''; } /** * This function prints a list item with a checkbox and an unordered list * of all the elements inside it. */ function theme_checkbox_tree_level($variables) { $element = $variables['element']; $sm = ''; if (array_key_exists('#level_start_minimized', $element) && $element['#level_start_minimized']) { $sm = ' style="display: none;"'; } $output = ''; return $output; } /** * This function prints a single item in the tree, followed by that item's * children (which may be another checkbox_tree_level). */ function theme_checkbox_tree_item($variables) { $element = $variables['element']; $children = Element::children($element); $output = ''; $sm = $element['#level_start_minimized'] ? ' term-reference-tree-collapsed' : ''; if (is_array($children) && count($children) > 1) { $output .= '
'; } elseif (!$element['#leaves_only']) { $output .= '
'; } foreach ($children as $child) { $output .= drupal_render($element[$child]); } return $output; } /** * This function prints a label that cannot be selected. */ function theme_checkbox_tree_label($variables) { $element = $variables['element']; $output = '
' . $element['#value'] . '
'; return $output; } /** * This function returns a taxonomy term hierarchy in a nested array. * * @param $tid * The ID of the root term. * @param $vid * The vocabulary ID to restrict the child search. * * @return * A nested array of the term's child objects. */ function _term_reference_tree_get_term_hierarchy($tid, $vid, &$allowed, $filter, $label, $default = array()) { $terms = _term_reference_tree_get_children($tid, $vid); $result = []; if ($filter != '') { foreach ($allowed as $k => $v) { if (array_key_exists($k, $terms)) { $term =& $terms[$k]; $children = _term_reference_tree_get_term_hierarchy($term->tid, $vid, $allowed, $filter, $label, $default); if (is_array($children)) { $term->children = $children; $term->children_selected = _term_reference_tree_children_selected($term, $default); } else { $term->children_selected = FALSE; } $term->TEST = $label; array_push($result, $term); } } } else { foreach ($terms as &$term) { if ($filter == '' || array_key_exists($term->tid, $allowed)) { $children = _term_reference_tree_get_term_hierarchy($term->tid, $vid, $allowed, $filter, $label, $default); if (is_array($children)) { $term->children = $children; $term->children_selected = _term_reference_tree_children_selected($term, $default); } else { $term->children_selected = FALSE; } $term->TEST = $label; array_push($result, $term); } } } return $result; } /** * This function is like taxonomy_get_children, except it doesn't load the * entire term. * * @param int $tid * The ID of the term whose children you want to get. * @param int $vid * The vocabulary ID. * * @return array * Taxonomy terms, each in the form ['tid' => $tid, 'name' => $name]. */ function _term_reference_tree_get_children($tid, $vid) { // DO NOT LOAD TAXONOMY TERMS HERE. // Taxonomy terms take a lot of time and memory to load, and this can be // very bad on large vocabularies. Instead, we load the term as necessary // in cases where it's needed (such as using tokens or when the locale // module is enabled). $table = 'taxonomy_term_field_data'; $alias = 't'; $query = \Drupal::database() ->select($table, $alias); $query->join('taxonomy_term__parent', 'p', 't.tid = p.entity_id'); $query->fields('t', ['tid', 'name']); $query->addField('t', 'vid', 'vocabulary_machine_name'); $query ->condition('t.vid', $vid) ->condition('p.parent_target_id', $tid) ->addTag('term_access') ->addTag('translatable') ->orderBy('t.weight') ->orderBy('t.name'); $result = $query->execute(); $terms = []; while ($term = $result->fetchObject()) { $terms[$term->tid] = $term; } return $terms; } function _term_reference_tree_children_selected($terms, $default) { foreach ($terms->children as $term) { if (isset($default[$term->tid]) || $term->children_selected) { return TRUE; } } return FALSE; } /** * Recursively go through the option tree and return a flat array of options. */ function _term_reference_tree_flatten($element, &$form_state) { $output = array(); $children = \Drupal\Core\Render\Element::children($element); foreach ($children as $c) { $child = $element[$c]; if (array_key_exists('#type', $child) && ($child['#type'] == 'radio' || $child['#type'] == 'checkbox')) { $output[] = $child; } else { $output = array_merge($output, _term_reference_tree_flatten($child, $form_state)); } } return $output; } /** * Return an array of options. * * This function converts a list of taxonomy terms to a key/value list of * options. */ function _term_reference_tree_get_options(&$terms, &$allowed, $filter) { $options = array(); if (is_array($terms) && count($terms) > 0) { foreach ($terms as $term) { if (!$filter || (is_array($allowed) && $allowed[$term->tid])) { $options[$term->tid] = $term->name; $options += _term_reference_tree_get_options($term->children, $allowed, $filter); } } } return $options; } /** * Builds a level in the term reference tree widget. * * This function returns an element that has a number of checkbox_tree_item * elements as children. It is meant to be called recursively when the widget * is built. */ function _term_reference_tree_build_level($element, $term, $form_state, $value, $max_choices, $parent_tids, $depth) { $start_minimized = TRUE; $leaves_only = FALSE; $container = array( '#type' => 'checkbox_tree_level', '#max_choices' => $max_choices, '#leaves_only' => $leaves_only, '#start_minimized' => $start_minimized, '#depth' => $depth, ); $container['#level_start_minimized'] = $depth > 1 && $element['#start_minimized'] && !($term->children_selected); foreach ($term->children as $child) { $container[$child->tid] = _term_reference_tree_build_item($element, $child, $form_state, $value, $max_choices, $parent_tids, $container, $depth); } return $container; } /** * Builds a single item in the term reference tree widget. * * This function returns an element with a checkbox for a single taxonomy term. * If that term has children, it appends checkbox_tree_level element that * contains the children. It is meant to be called recursively when the widget * is built. */ function _term_reference_tree_build_item($element, $term, $form_state, $value, $max_choices, $parent_tids, $parent, $depth) { $leaves_only = FALSE; $langcode = \Drupal::languageManager()->getCurrentLanguage()->getId(); $t = NULL; if (\Drupal::moduleHandler()->moduleExists('locale') && !empty($term->tid)) { $t = \Drupal::entityManager() ->getStorage('taxonomy_term') ->load($term->tid); if ($t && $t->hasTranslation($langcode)) { $term_name = $t->getTranslation($langcode)->label(); } } if (empty($term_name)) { $term_name = $term->name; } $container = array( '#type' => 'checkbox_tree_item', '#max_choices' => $max_choices, '#leaves_only' => $leaves_only, '#term_name' => $term_name, '#level_start_minimized' => FALSE, '#select_parents' => $element['#select_parents'], '#depth' => $depth, ); if (!$element['#leaves_only'] || count($term->children) == 0) { $e = array( '#type' => ($max_choices == 1) ? 'radio' : 'checkbox', '#title' => $term_name, '#on_value' => $term->tid, '#off_value' => 0, '#return_value' => $term->tid, '#parent_values' => $parent_tids, '#default_value' => isset($value[$term->tid]) ? $term->tid : NULL, '#attributes' => isset($element['#attributes']) ? $element['#attributes'] : NULL, '#ajax' => array( 'callback' => '_term_reference_tree_item_changed_ajax_callback', 'event' => 'change', 'message' => '', ) ); if ($e['#type'] == 'radio') { $parents_for_id = array_merge($element['#parents'], array($term->tid)); $e['#id'] = \Drupal\Component\Utility\Html::getId('edit-' . implode('-', $parents_for_id)); $e['#parents'] = $element['#parents']; } } else { $e = array( '#type' => 'checkbox_tree_label', '#value' => $term_name, ); } $container[$term->tid] = $e; if (($depth + 1 <= $element['#max_depth'] || !$element['#max_depth']) && property_exists($term, 'children') && count($term->children) > 0) { $parents = $parent_tids; $parents[] = $term->tid; $container[$term->tid . '-children'] = _term_reference_tree_build_level($element, $term, $form_state, $value, $max_choices, $parents, $depth + 1); $container['#level_start_minimized'] = $container[$term->tid . '-children']['#level_start_minimized']; } return $container; } /** * Themes the term tree display (as opposed to the select widget). */ function theme_term_tree_list($variables) { $element = &$variables['element']; $data = &$element['#data']; $tree = []; // For each selected term. foreach ($data as $item) { // Loop if the term ID is not zero. $values = []; $tid = $item['target_id']; $original_tid = $tid; while ($tid != 0) { // Unshift the term onto an array. array_unshift($values, $tid); // Repeat with parent term. $tid = _term_reference_tree_get_parent($tid); } $current = &$tree; // For each term in the above array. foreach ($values as $tid) { // current[children][term_id] = new array. if (!isset($current['children'][$tid])) { $current['children'][$tid] = ['selected' => FALSE]; } // If this is the last value in the array, // tree[children][term_id][selected] = true. if ($tid == $original_tid) { $current['children'][$tid]['selected'] = TRUE; } $current['children'][$tid]['tid'] = $tid; $current = &$current['children'][$tid]; } } $output = '
'; $output .= _term_reference_tree_output_list_level($element, $tree); $output .= '
'; return $output; } /** * Helper function to get the parent of tid. * * @param int $tid * The term id. * * @return int * Parent term id or 0. */ function _term_reference_tree_get_parent($tid) { $query = "SELECT p.parent_target_id FROM {taxonomy_term__parent} p WHERE p.entity_id = :tid"; $from = 0; $count = 1; $args = [':tid' => $tid]; $database = \Drupal::database(); $result = $database->queryRange($query, $from, $count, $args); $parent_tid = 0; foreach ($result as $term) { $parent_tid = $term->parent_target_id; } return $parent_tid; } /** * Helper function to output a single level of the term reference tree display. */ function _term_reference_tree_output_list_level(&$element, &$tree) { $output = ''; $langcode = \Drupal::languageManager()->getCurrentLanguage()->getId(); if (isset($tree['children']) && is_array($tree['children']) && count($tree['children']) > 0) { $output = ''; } return $output; } /** * Helper function to output a dragtable as track_list. */ function _term_reference_tree_build_track_list_order($value, $options){ // define the tabledrag container $table = array( '#type' => 'table', '#prefix' => '', '#header' => array('Label', 'weight', 'remove'), '#tabledrag' => array( array( 'action' => 'order', 'relationship' => 'sibling', 'group' => 'tracklist-order-weight', ), ), ); // define the table rows $index = 0; foreach ($value as $key) { $table[$key] = [ '#attributes' => array( 'class' => ['draggable'] ), '#weight' => $index, 'label' => array( '#plain_text' => $options[$key], ), 'weight' => array( '#type' => 'weight', '#title' => t('Weight for @title', array('@title' => $options[$key])), '#title_display' => 'invisible', '#default_value' => $index, // Classify the weight element for #tabledrag. '#attributes' => array('class' => array('tracklist-order-weight')), ), 'remove' => array( '#type' => 'button', '#value' => 'remove', '#attributes' => array( 'term-reference-tree-key' => $key, ) ) ]; $index ++; } return $table; } function _term_reference_tree_item_changed_ajax_callback(array &$form, FormState $form_state){ $trigger = $form_state->getTriggeringElement(); // get the trigger element (a term_ref_tree checkbox) $parent = $trigger['#parents'][0]; // get the element's parent field name $track_list_form = $form[$parent]['widget']['track_list']; // extract the track_list form part // ajax response $response = new AjaxResponse(); $response->addCommand(new ReplaceCommand('.term-reference-tree-track-list', $track_list_form)); return $response; } function _term_reference_tree_get_flatten_selected_values($tree){ $selected = []; foreach ($tree as $key => $value) { if ( array_key_exists($key, $value) ) { if( $value[$key] == $key ){ $selected[] = $key; } } if( array_key_exists($key.'-children', $value) ){ $selected += _term_reference_tree_get_flatten_selected_values($value[$key.'-children']); } } return $selected; }