TermReferenceTree.php 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. <?php
  2. namespace Drupal\term_reference_tree\Plugin\Field\FieldWidget;
  3. use Drupal\Component\Utility\NestedArray;
  4. use Drupal\Core\Field\FieldItemListInterface;
  5. use Drupal\Core\Field\WidgetBase;
  6. use Drupal\Core\Form\FormStateInterface;
  7. use Drupal\field\Entity\FieldStorageConfig;
  8. use Drupal\taxonomy\Entity\Vocabulary;
  9. /**
  10. * Plugin implementation of the 'term_reference_tree' widget.
  11. *
  12. * @FieldWidget(
  13. * id = "term_reference_tree",
  14. * label = @Translation("Term reference tree"),
  15. * field_types = {"entity_reference"},
  16. * multiple_values = TRUE
  17. * )
  18. */
  19. class TermReferenceTree extends WidgetBase {
  20. const CASCADING_SELECTION_NONE = '0';
  21. const CASCADING_SELECTION_BOTH = '1';
  22. const CASCADING_SELECTION_SELECT = '2';
  23. const CASCADING_SELECTION_DESELECT = '3';
  24. /**
  25. * {@inheritdoc}
  26. */
  27. public static function defaultSettings() {
  28. return [
  29. 'start_minimized' => TRUE,
  30. 'leaves_only' => FALSE,
  31. 'select_parents' => FALSE,
  32. 'cascading_selection' => self::CASCADING_SELECTION_NONE,
  33. 'track_list' => FALSE,
  34. 'max_depth' => 0,
  35. ] + parent::defaultSettings();
  36. }
  37. /**
  38. * {@inheritdoc}
  39. */
  40. public function settingsForm(array $form, FormStateInterface $form_state) {
  41. $form = parent::settingsForm($form, $form_state);
  42. $form['start_minimized'] = [
  43. '#type' => 'checkbox',
  44. '#title' => $this->t('Start minimized'),
  45. '#description' => $this->t('Make the tree appear minimized on the form by default'),
  46. '#default_value' => $this->getSetting('start_minimized'),
  47. ];
  48. $form['leaves_only'] = [
  49. '#type' => 'checkbox',
  50. '#title' => $this->t('Leaves only'),
  51. '#description' => $this->t("Don't allow the user to select items that have children"),
  52. '#default_value' => $this->getSetting('leaves_only'),
  53. '#return_value' => 1,
  54. ];
  55. $form['select_parents'] = [
  56. '#type' => 'checkbox',
  57. '#title' => $this->t('Select parents automatically'),
  58. '#description' => $this->t("When turned on, this option causes the widget to automatically select the ancestors of all selected items. In Leaves Only mode, the parents will be added invisibly to the selected value. <em>This option is only valid if an unlimited number of values can be selected.</em>"),
  59. '#default_value' => $this->getSetting('select_parents'),
  60. '#return_value' => 1,
  61. ];
  62. $form['cascading_selection'] = [
  63. '#type' => 'select',
  64. '#title' => $this->t('Cascading selection'),
  65. '#description' => $this->t('On parent selection, automatically select children if none were selected. Some may then be manually unselected. In the same way, on parent unselection, unselect children if all were selected. <em>This option is only valid if an unlimited number of values can be selected.</em>'),
  66. '#default_value' => $this->getSetting('cascading_selection'),
  67. '#options' => [
  68. self::CASCADING_SELECTION_NONE => $this->t('None'),
  69. self::CASCADING_SELECTION_BOTH => $this->t('Select / deselect'),
  70. self::CASCADING_SELECTION_SELECT => $this->t('Only select'),
  71. self::CASCADING_SELECTION_DESELECT => $this->t('Only deselect'),
  72. ],
  73. ];
  74. if ($this->fieldDefinition->getFieldStorageDefinition()
  75. ->getCardinality() !== FieldStorageConfig::CARDINALITY_UNLIMITED) {
  76. $form['select_parents']['#disabled'] = TRUE;
  77. $form['select_parents']['#default_value'] = FALSE;
  78. $form['select_parents']['#description'] .= ' <em>' . $this->t("This option is only valid if an unlimited number of values can be selected.") . '</em>';
  79. $form['cascading_selection']['#disabled'] = TRUE;
  80. $form['cascading_selection']['#default_value'] = self::CASCADING_SELECTION_NONE;
  81. $form['cascading_selection']['#description'] .= ' <em>' . $this->t("This option is only valid if an unlimited number of values can be selected.") . '</em>';
  82. }
  83. $form['track_list'] = [
  84. '#type' => 'checkbox',
  85. '#title' => $this->t('Track list'),
  86. '#description' => $this->t('Track what the user has chosen in a list below the tree.
  87. Useful when the tree is large, with many levels.'),
  88. '#default_value' => $this->getSetting('track_list'),
  89. '#return_value' => 1,
  90. ];
  91. $form['max_depth'] = [
  92. '#type' => 'number',
  93. '#title' => $this->t('Maximum Depth'),
  94. '#description' => $this->t("Only show items up to this many levels deep."),
  95. '#default_value' => $this->getSetting('max_depth'),
  96. '#min' => 0,
  97. ];
  98. return $form;
  99. }
  100. /**
  101. * {@inheritdoc}
  102. */
  103. public function settingsSummary() {
  104. $summary = [];
  105. if ($this->getSetting('start_minimized')) {
  106. $summary[] = $this->t('Start minimized');
  107. }
  108. if ($this->getSetting('leaves_only')) {
  109. $summary[] = $this->t('Leaves only');
  110. }
  111. if ($this->getSetting('select_parents')) {
  112. $summary[] = $this->t('Select parents automatically');
  113. }
  114. $cascadingSelection = $this->getSetting('cascading_selection');
  115. if ($cascadingSelection == self::CASCADING_SELECTION_BOTH) {
  116. $summary[] = $this->t('Cascading selection');
  117. }
  118. elseif ($cascadingSelection == self::CASCADING_SELECTION_SELECT) {
  119. $summary[] = sprintf('%s (%s)', $this->t('Cascading selection'), $this->t('Only select'));
  120. }
  121. elseif ($cascadingSelection == self::CASCADING_SELECTION_DESELECT) {
  122. $summary[] = sprintf('%s (%s)', $this->t('Cascading selection'), $this->t('Only deselect'));
  123. }
  124. if ($this->getSetting('track_list')) {
  125. $summary[] = $this->t('Track list');
  126. }
  127. if ($this->getSetting('max_depth')) {
  128. $summary[] = $this->formatPlural($this->getSetting('max_depth'), 'Maximum Depth: @count level', 'Maximum Depth: @count levels');
  129. }
  130. return $summary;
  131. }
  132. /**
  133. * {@inheritdoc}
  134. */
  135. public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
  136. $handler_settings = $this->getFieldSetting('handler_settings');
  137. $vocabularies = Vocabulary::loadMultiple($handler_settings['target_bundles']);
  138. $element['#type'] = 'checkbox_tree';
  139. $element['#default_value'] = $items->getValue();
  140. $element['#vocabularies'] = $vocabularies;
  141. $element['#max_choices'] = $this->fieldDefinition->getFieldStorageDefinition()
  142. ->getCardinality();
  143. $element['#leaves_only'] = $this->getSetting('leaves_only');
  144. $element['#select_parents'] = $this->getSetting('select_parents');
  145. $element['#cascading_selection'] = $this->getSetting('cascading_selection');
  146. $element['#track_list'] = $this->getSetting('track_list');
  147. $element['#value_key'] = 'target_id';
  148. $element['#max_depth'] = $this->getSetting('max_depth');
  149. $element['#start_minimized'] = $this->getSetting('start_minimized');
  150. $element['#element_validate'] = [
  151. [
  152. get_class($this),
  153. 'validateTermReferenceTreeElement',
  154. ],
  155. ];
  156. return $element;
  157. }
  158. /**
  159. * Form element validation handler for term reference form widget.
  160. */
  161. public static function validateTermReferenceTreeElement(&$element, FormStateInterface $form_state) {
  162. $items = _term_reference_tree_flatten($element, $form_state);
  163. $value = [];
  164. if ($element['#max_choices'] != 1) {
  165. foreach ($items as $child) {
  166. if (!empty($child['#value'])) {
  167. // If the element is leaves only and select parents is on,
  168. // then automatically add all the parents of each selected value.
  169. if (!empty($element['#select_parents']) && !empty($element['#leaves_only'])) {
  170. foreach ($child['#parent_values'] as $parent_tid) {
  171. if (!in_array([$element['#value_key'] => $parent_tid], $value)) {
  172. array_push($value, [$element['#value_key'] => $parent_tid]);
  173. }
  174. }
  175. }
  176. array_push($value, [$element['#value_key'] => $child['#value']]);
  177. }
  178. }
  179. // if track_list enabled, reorder the value array regarding the tablegrag input
  180. if (!empty($value) && $element['#track_list']) {
  181. $input = &$form_state->getUserInput();
  182. $nested_input = NestedArray::getValue($form_state->getValues(), $element['#parents']);
  183. if (is_array($nested_input['track_list'])) {
  184. $track_list = array_keys($nested_input['track_list']);
  185. $sorted_value = [];
  186. foreach ($track_list as $id) {
  187. $sorted_value[] = $value[array_search($id, array_column($value, 'target_id'))];
  188. }
  189. $value = $sorted_value;
  190. }
  191. }
  192. }
  193. else {
  194. // If it's a tree of radio buttons, they all have the same value,
  195. // so we can just grab the value of the first one.
  196. if (count($items) > 0) {
  197. $child = reset($items);
  198. if (!empty($child['#value'])) {
  199. array_push($value, [$element['#value_key'] => $child['#value']]);
  200. }
  201. }
  202. }
  203. if ($element['#required'] && empty($value)) {
  204. $form_state->setError($element, t('%name field is required.', ['%name' => $element['#title']]));
  205. }
  206. $form_state->setValueForElement($element, $value);
  207. }
  208. }