term_reference_tree.module 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580
  1. <?php
  2. use \Drupal\Core\Render\Element;
  3. use Drupal\Core\Form\FormState;
  4. use Drupal\Core\Ajax\AjaxResponse;
  5. use Drupal\Core\Ajax\ReplaceCommand;
  6. /**
  7. * Implements hook_theme().
  8. */
  9. function term_reference_tree_theme() {
  10. return [
  11. 'checkbox_tree' => [
  12. 'render element' => 'element',
  13. 'function' => 'theme_checkbox_tree',
  14. ],
  15. 'checkbox_tree_level' => [
  16. 'render element' => 'element',
  17. 'function' => 'theme_checkbox_tree_level',
  18. ],
  19. 'checkbox_tree_item' => [
  20. 'render element' => 'element',
  21. 'function' => 'theme_checkbox_tree_item',
  22. ],
  23. 'checkbox_tree_label' => [
  24. 'render element' => 'element',
  25. 'function' => 'theme_checkbox_tree_label',
  26. ],
  27. 'term_tree_list' => [
  28. 'render element' => 'element',
  29. 'function' => 'theme_term_tree_list',
  30. ],
  31. ];
  32. }
  33. /**
  34. * Returns HTML for a checkbox_tree form element.
  35. */
  36. function theme_checkbox_tree($variables) {
  37. $element = $variables['element'];
  38. $element['#children'] = drupal_render_children($element);
  39. $attributes = isset($element['#attributes']) ? $element['#attributes'] : [];
  40. if (isset($element['#id'])) {
  41. $attributes['id'] = $element['#id'];
  42. }
  43. $attributes['class'][] = 'term-reference-tree';
  44. if (!empty($element['#required'])) {
  45. $attributes['class'][] = 'required';
  46. }
  47. if (array_key_exists('#start_minimized', $element) && $element['#start_minimized']) {
  48. $attributes['class'][] = 'term-reference-tree-collapsed';
  49. }
  50. if (array_key_exists('#track_list', $element) && $element['#track_list']) {
  51. $attributes['class'][] = 'term-reference-tree-track-list-shown';
  52. }
  53. if (!empty($variables['element']['#select_parents'])) {
  54. $attributes['class'][] = 'term-reference-tree-select-parents';
  55. }
  56. if ($variables['element']['#cascading_selection'] != \Drupal\term_reference_tree\Plugin\Field\FieldWidget\TermReferenceTree::CASCADING_SELECTION_NONE) {
  57. $attributes['class'][] = 'term-reference-tree-cascading-selection';
  58. }
  59. if ($variables['element']['#cascading_selection'] == \Drupal\term_reference_tree\Plugin\Field\FieldWidget\TermReferenceTree::CASCADING_SELECTION_SELECT) {
  60. $attributes['class'][] = 'term-reference-tree-cascading-selection-mode-select';
  61. }
  62. else {
  63. if ($variables['element']['#cascading_selection'] == \Drupal\term_reference_tree\Plugin\Field\FieldWidget\TermReferenceTree::CASCADING_SELECTION_DESELECT) {
  64. $attributes['class'][] = 'term-reference-tree-cascading-selection-mode-deselect';
  65. }
  66. }
  67. if (!empty($element['#attributes']['class'])) {
  68. $attributes['class'] = array_merge($attributes['class'], $element['#attributes']['class']);
  69. }
  70. return
  71. '<div' . new \Drupal\Core\Template\Attribute($attributes) . '>'
  72. . (!empty($element['#children']) ? $element['#children'] : '')
  73. . '</div>';
  74. }
  75. /**
  76. * This function prints a list item with a checkbox and an unordered list
  77. * of all the elements inside it.
  78. */
  79. function theme_checkbox_tree_level($variables) {
  80. $element = $variables['element'];
  81. $sm = '';
  82. if (array_key_exists('#level_start_minimized', $element) && $element['#level_start_minimized']) {
  83. $sm = ' style="display: none;"';
  84. }
  85. $output = '<ul class="term-reference-tree-level "' . $sm . '>';
  86. $children = Element::children($element);
  87. foreach ($children as $child) {
  88. $output .= '<li>';
  89. $output .= \Drupal::service('renderer')->render($element[$child]);
  90. $output .= '</li>';
  91. }
  92. $output .= '</ul>';
  93. return $output;
  94. }
  95. /**
  96. * This function prints a single item in the tree, followed by that item's
  97. * children (which may be another checkbox_tree_level).
  98. */
  99. function theme_checkbox_tree_item($variables) {
  100. $element = $variables['element'];
  101. $children = Element::children($element);
  102. $output = '';
  103. $sm = $element['#level_start_minimized'] ? ' term-reference-tree-collapsed' : '';
  104. if (is_array($children) && count($children) > 1) {
  105. $output .= '<div class="term-reference-tree-button' . $sm . '"></div>';
  106. }
  107. elseif (!$element['#leaves_only']) {
  108. $output .= '<div class="no-term-reference-tree-button"></div>';
  109. }
  110. foreach ($children as $child) {
  111. $output .= drupal_render($element[$child]);
  112. }
  113. return $output;
  114. }
  115. /**
  116. * This function prints a label that cannot be selected.
  117. */
  118. function theme_checkbox_tree_label($variables) {
  119. $element = $variables['element'];
  120. $output = '<div class="parent-term">' . $element['#value'] . '</div>';
  121. return $output;
  122. }
  123. /**
  124. * This function returns a taxonomy term hierarchy in a nested array.
  125. *
  126. * @param $tid
  127. * The ID of the root term.
  128. * @param $vid
  129. * The vocabulary ID to restrict the child search.
  130. *
  131. * @return
  132. * A nested array of the term's child objects.
  133. */
  134. function _term_reference_tree_get_term_hierarchy($tid, $vid, &$allowed, $filter, $label, $default = array()) {
  135. $terms = _term_reference_tree_get_children($tid, $vid);
  136. $result = [];
  137. if ($filter != '') {
  138. foreach ($allowed as $k => $v) {
  139. if (array_key_exists($k, $terms)) {
  140. $term =& $terms[$k];
  141. $children = _term_reference_tree_get_term_hierarchy($term->tid, $vid, $allowed, $filter, $label, $default);
  142. if (is_array($children)) {
  143. $term->children = $children;
  144. $term->children_selected = _term_reference_tree_children_selected($term, $default);
  145. }
  146. else {
  147. $term->children_selected = FALSE;
  148. }
  149. $term->TEST = $label;
  150. array_push($result, $term);
  151. }
  152. }
  153. }
  154. else {
  155. foreach ($terms as &$term) {
  156. if ($filter == '' || array_key_exists($term->tid, $allowed)) {
  157. $children = _term_reference_tree_get_term_hierarchy($term->tid, $vid, $allowed, $filter, $label, $default);
  158. if (is_array($children)) {
  159. $term->children = $children;
  160. $term->children_selected = _term_reference_tree_children_selected($term, $default);
  161. }
  162. else {
  163. $term->children_selected = FALSE;
  164. }
  165. $term->TEST = $label;
  166. array_push($result, $term);
  167. }
  168. }
  169. }
  170. return $result;
  171. }
  172. /**
  173. * This function is like taxonomy_get_children, except it doesn't load the
  174. * entire term.
  175. *
  176. * @param int $tid
  177. * The ID of the term whose children you want to get.
  178. * @param int $vid
  179. * The vocabulary ID.
  180. *
  181. * @return array
  182. * Taxonomy terms, each in the form ['tid' => $tid, 'name' => $name].
  183. */
  184. function _term_reference_tree_get_children($tid, $vid) {
  185. // DO NOT LOAD TAXONOMY TERMS HERE.
  186. // Taxonomy terms take a lot of time and memory to load, and this can be
  187. // very bad on large vocabularies. Instead, we load the term as necessary
  188. // in cases where it's needed (such as using tokens or when the locale
  189. // module is enabled).
  190. $table = 'taxonomy_term_field_data';
  191. $alias = 't';
  192. $query = \Drupal::database()
  193. ->select($table, $alias);
  194. $query->join('taxonomy_term__parent', 'p', 't.tid = p.entity_id');
  195. $query->fields('t', ['tid', 'name']);
  196. $query->addField('t', 'vid', 'vocabulary_machine_name');
  197. $query
  198. ->condition('t.vid', $vid)
  199. ->condition('p.parent_target_id', $tid)
  200. ->addTag('term_access')
  201. ->addTag('translatable')
  202. ->orderBy('t.weight')
  203. ->orderBy('t.name');
  204. $result = $query->execute();
  205. $terms = [];
  206. while ($term = $result->fetchObject()) {
  207. $terms[$term->tid] = $term;
  208. }
  209. return $terms;
  210. }
  211. function _term_reference_tree_children_selected($terms, $default) {
  212. foreach ($terms->children as $term) {
  213. if (isset($default[$term->tid]) || $term->children_selected) {
  214. return TRUE;
  215. }
  216. }
  217. return FALSE;
  218. }
  219. /**
  220. * Recursively go through the option tree and return a flat array of options.
  221. */
  222. function _term_reference_tree_flatten($element, &$form_state) {
  223. $output = array();
  224. $children = \Drupal\Core\Render\Element::children($element);
  225. foreach ($children as $c) {
  226. $child = $element[$c];
  227. if (array_key_exists('#type', $child) && ($child['#type'] == 'radio' || $child['#type'] == 'checkbox')) {
  228. $output[] = $child;
  229. }
  230. else {
  231. $output = array_merge($output, _term_reference_tree_flatten($child, $form_state));
  232. }
  233. }
  234. return $output;
  235. }
  236. /**
  237. * Return an array of options.
  238. *
  239. * This function converts a list of taxonomy terms to a key/value list of
  240. * options.
  241. */
  242. function _term_reference_tree_get_options(&$terms, &$allowed, $filter) {
  243. $options = array();
  244. if (is_array($terms) && count($terms) > 0) {
  245. foreach ($terms as $term) {
  246. if (!$filter || (is_array($allowed) && $allowed[$term->tid])) {
  247. $options[$term->tid] = $term->name;
  248. $options += _term_reference_tree_get_options($term->children, $allowed, $filter);
  249. }
  250. }
  251. }
  252. return $options;
  253. }
  254. /**
  255. * Builds a level in the term reference tree widget.
  256. *
  257. * This function returns an element that has a number of checkbox_tree_item
  258. * elements as children. It is meant to be called recursively when the widget
  259. * is built.
  260. */
  261. function _term_reference_tree_build_level($element, $term, $form_state, $value, $max_choices, $parent_tids, $depth) {
  262. $start_minimized = TRUE;
  263. $leaves_only = FALSE;
  264. $container = array(
  265. '#type' => 'checkbox_tree_level',
  266. '#max_choices' => $max_choices,
  267. '#leaves_only' => $leaves_only,
  268. '#start_minimized' => $start_minimized,
  269. '#depth' => $depth,
  270. );
  271. $container['#level_start_minimized'] = $depth > 1 && $element['#start_minimized'] && !($term->children_selected);
  272. foreach ($term->children as $child) {
  273. $container[$child->tid] = _term_reference_tree_build_item($element, $child, $form_state, $value, $max_choices, $parent_tids, $container, $depth);
  274. }
  275. return $container;
  276. }
  277. /**
  278. * Builds a single item in the term reference tree widget.
  279. *
  280. * This function returns an element with a checkbox for a single taxonomy term.
  281. * If that term has children, it appends checkbox_tree_level element that
  282. * contains the children. It is meant to be called recursively when the widget
  283. * is built.
  284. */
  285. function _term_reference_tree_build_item($element, $term, $form_state, $value, $max_choices, $parent_tids, $parent, $depth) {
  286. $leaves_only = FALSE;
  287. $langcode = \Drupal::languageManager()->getCurrentLanguage()->getId();
  288. $t = NULL;
  289. if (\Drupal::moduleHandler()->moduleExists('locale') && !empty($term->tid)) {
  290. $t = \Drupal::entityManager()
  291. ->getStorage('taxonomy_term')
  292. ->load($term->tid);
  293. if ($t && $t->hasTranslation($langcode)) {
  294. $term_name = $t->getTranslation($langcode)->label();
  295. }
  296. }
  297. if (empty($term_name)) {
  298. $term_name = $term->name;
  299. }
  300. $container = array(
  301. '#type' => 'checkbox_tree_item',
  302. '#max_choices' => $max_choices,
  303. '#leaves_only' => $leaves_only,
  304. '#term_name' => $term_name,
  305. '#level_start_minimized' => FALSE,
  306. '#select_parents' => $element['#select_parents'],
  307. '#depth' => $depth,
  308. );
  309. if (!$element['#leaves_only'] || count($term->children) == 0) {
  310. $e = array(
  311. '#type' => ($max_choices == 1) ? 'radio' : 'checkbox',
  312. '#title' => $term_name,
  313. '#on_value' => $term->tid,
  314. '#off_value' => 0,
  315. '#return_value' => $term->tid,
  316. '#parent_values' => $parent_tids,
  317. '#default_value' => isset($value[$term->tid]) ? $term->tid : NULL,
  318. '#attributes' => isset($element['#attributes']) ? $element['#attributes'] : NULL,
  319. '#ajax' => array(
  320. 'callback' => '_term_reference_tree_item_changed_ajax_callback',
  321. 'event' => 'change',
  322. 'message' => '',
  323. )
  324. );
  325. if ($e['#type'] == 'radio') {
  326. $parents_for_id = array_merge($element['#parents'], array($term->tid));
  327. $e['#id'] = \Drupal\Component\Utility\Html::getId('edit-' . implode('-', $parents_for_id));
  328. $e['#parents'] = $element['#parents'];
  329. }
  330. }
  331. else {
  332. $e = array(
  333. '#type' => 'checkbox_tree_label',
  334. '#value' => $term_name,
  335. );
  336. }
  337. $container[$term->tid] = $e;
  338. if (($depth + 1 <= $element['#max_depth'] || !$element['#max_depth']) && property_exists($term, 'children') && count($term->children) > 0) {
  339. $parents = $parent_tids;
  340. $parents[] = $term->tid;
  341. $container[$term->tid . '-children'] = _term_reference_tree_build_level($element, $term, $form_state, $value, $max_choices, $parents, $depth + 1);
  342. $container['#level_start_minimized'] = $container[$term->tid . '-children']['#level_start_minimized'];
  343. }
  344. return $container;
  345. }
  346. /**
  347. * Themes the term tree display (as opposed to the select widget).
  348. */
  349. function theme_term_tree_list($variables) {
  350. $element = &$variables['element'];
  351. $data = &$element['#data'];
  352. $tree = [];
  353. // For each selected term.
  354. foreach ($data as $item) {
  355. // Loop if the term ID is not zero.
  356. $values = [];
  357. $tid = $item['target_id'];
  358. $original_tid = $tid;
  359. while ($tid != 0) {
  360. // Unshift the term onto an array.
  361. array_unshift($values, $tid);
  362. // Repeat with parent term.
  363. $tid = _term_reference_tree_get_parent($tid);
  364. }
  365. $current = &$tree;
  366. // For each term in the above array.
  367. foreach ($values as $tid) {
  368. // current[children][term_id] = new array.
  369. if (!isset($current['children'][$tid])) {
  370. $current['children'][$tid] = ['selected' => FALSE];
  371. }
  372. // If this is the last value in the array,
  373. // tree[children][term_id][selected] = true.
  374. if ($tid == $original_tid) {
  375. $current['children'][$tid]['selected'] = TRUE;
  376. }
  377. $current['children'][$tid]['tid'] = $tid;
  378. $current = &$current['children'][$tid];
  379. }
  380. }
  381. $output = '<div class="term-tree-list">';
  382. $output .= _term_reference_tree_output_list_level($element, $tree);
  383. $output .= '</div>';
  384. return $output;
  385. }
  386. /**
  387. * Helper function to get the parent of tid.
  388. *
  389. * @param int $tid
  390. * The term id.
  391. *
  392. * @return int
  393. * Parent term id or 0.
  394. */
  395. function _term_reference_tree_get_parent($tid) {
  396. $query = "SELECT p.parent_target_id FROM {taxonomy_term__parent} p WHERE p.entity_id = :tid";
  397. $from = 0;
  398. $count = 1;
  399. $args = [':tid' => $tid];
  400. $database = \Drupal::database();
  401. $result = $database->queryRange($query, $from, $count, $args);
  402. $parent_tid = 0;
  403. foreach ($result as $term) {
  404. $parent_tid = $term->parent_target_id;
  405. }
  406. return $parent_tid;
  407. }
  408. /**
  409. * Helper function to output a single level of the term reference tree display.
  410. */
  411. function _term_reference_tree_output_list_level(&$element, &$tree) {
  412. $output = '';
  413. $langcode = \Drupal::languageManager()->getCurrentLanguage()->getId();
  414. if (isset($tree['children']) && is_array($tree['children']) && count($tree['children']) > 0) {
  415. $output = '<ul class="term">';
  416. foreach ($tree['children'] as &$item) {
  417. if (isset($item['tid'])) {
  418. $term = \Drupal\taxonomy\Entity\Term::load($item['tid']);
  419. $url = $term->toUrl();
  420. $uri['options']['html'] = TRUE;
  421. $class = $item['selected'] ? 'selected' : 'unselected';
  422. $output .= '<li class="' . $class . '">';
  423. $t = NULL;
  424. $term_name = '';
  425. if (\Drupal::moduleHandler()
  426. ->moduleExists('locale') && !empty($term->tid)) {
  427. $t = $term;
  428. if ($t && $t->hasTranslation($langcode)) {
  429. $term_name = $t->getTranslation($langcode)->label();
  430. }
  431. }
  432. if (empty($term_name)) {
  433. $term_name = $term->label();
  434. }
  435. $output .= \Drupal::service('link_generator')
  436. ->generate($term_name, $url);
  437. if (isset($item['children'])) {
  438. $output .= _term_reference_tree_output_list_level($element, $item);
  439. }
  440. $output .= '</li>';
  441. }
  442. }
  443. $output .= '</ul>';
  444. }
  445. return $output;
  446. }
  447. /**
  448. * Helper function to output a dragtable as track_list.
  449. */
  450. function _term_reference_tree_build_track_list_order($value, $options){
  451. // define the tabledrag container
  452. $table = array(
  453. '#type' => 'table',
  454. '#prefix' => '<aside class="term-reference-tree-track-list">',
  455. '#suffix' => '</aside>',
  456. '#header' => array('Label', 'weight', 'remove'),
  457. '#tabledrag' => array(
  458. array(
  459. 'action' => 'order',
  460. 'relationship' => 'sibling',
  461. 'group' => 'tracklist-order-weight',
  462. ),
  463. ),
  464. );
  465. // define the table rows
  466. $index = 0;
  467. foreach ($value as $key) {
  468. $table[$key] = [
  469. '#attributes' => array(
  470. 'class' => ['draggable']
  471. ),
  472. '#weight' => $index,
  473. 'label' => array(
  474. '#plain_text' => $options[$key],
  475. ),
  476. 'weight' => array(
  477. '#type' => 'weight',
  478. '#title' => t('Weight for @title', array('@title' => $options[$key])),
  479. '#title_display' => 'invisible',
  480. '#default_value' => $index,
  481. // Classify the weight element for #tabledrag.
  482. '#attributes' => array('class' => array('tracklist-order-weight')),
  483. ),
  484. 'remove' => array(
  485. '#type' => 'button',
  486. '#value' => 'remove',
  487. '#attributes' => array(
  488. 'term-reference-tree-key' => $key,
  489. )
  490. )
  491. ];
  492. $index ++;
  493. }
  494. return $table;
  495. }
  496. function _term_reference_tree_item_changed_ajax_callback(array &$form, FormState $form_state){
  497. $trigger = $form_state->getTriggeringElement(); // get the trigger element (a term_ref_tree checkbox)
  498. $parent = $trigger['#parents'][0]; // get the element's parent field name
  499. $track_list_form = $form[$parent]['widget']['track_list']; // extract the track_list form part
  500. // ajax response
  501. $response = new AjaxResponse();
  502. $response->addCommand(new ReplaceCommand('.term-reference-tree-track-list', $track_list_form));
  503. return $response;
  504. }
  505. function _term_reference_tree_get_flatten_selected_values($tree){
  506. $selected = [];
  507. foreach ($tree as $key => $value) {
  508. if ( array_key_exists($key, $value) ) {
  509. if( $value[$key] == $key ){
  510. $selected[] = $key;
  511. }
  512. }
  513. if( array_key_exists($key.'-children', $value) ){
  514. $selected += _term_reference_tree_get_flatten_selected_values($value[$key.'-children']);
  515. }
  516. }
  517. return $selected;
  518. }