3 namespace Drupal\taxonomy\Plugin\views\filter;
5 use Drupal\Core\Entity\Element\EntityAutocomplete;
6 use Drupal\Core\Form\FormStateInterface;
7 use Drupal\taxonomy\Entity\Term;
8 use Drupal\taxonomy\TermStorageInterface;
9 use Drupal\taxonomy\VocabularyStorageInterface;
10 use Drupal\views\ViewExecutable;
11 use Drupal\views\Plugin\views\display\DisplayPluginBase;
12 use Drupal\views\Plugin\views\filter\ManyToOne;
13 use Symfony\Component\DependencyInjection\ContainerInterface;
18 * @ingroup views_filter_handlers
20 * @ViewsFilter("taxonomy_index_tid")
22 class TaxonomyIndexTid extends ManyToOne {
24 // Stores the exposed input for this filter.
25 public $validated_exposed_input = NULL;
28 * The vocabulary storage.
30 * @var \Drupal\taxonomy\VocabularyStorageInterface
32 protected $vocabularyStorage;
37 * @var \Drupal\taxonomy\TermStorageInterface
39 protected $termStorage;
42 * Constructs a TaxonomyIndexTid object.
44 * @param array $configuration
45 * A configuration array containing information about the plugin instance.
46 * @param string $plugin_id
47 * The plugin_id for the plugin instance.
48 * @param mixed $plugin_definition
49 * The plugin implementation definition.
50 * @param \Drupal\taxonomy\VocabularyStorageInterface $vocabulary_storage
51 * The vocabulary storage.
52 * @param \Drupal\taxonomy\TermStorageInterface $term_storage
55 public function __construct(array $configuration, $plugin_id, $plugin_definition, VocabularyStorageInterface $vocabulary_storage, TermStorageInterface $term_storage) {
56 parent::__construct($configuration, $plugin_id, $plugin_definition);
57 $this->vocabularyStorage = $vocabulary_storage;
58 $this->termStorage = $term_storage;
64 public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
69 $container->get('entity.manager')->getStorage('taxonomy_vocabulary'),
70 $container->get('entity.manager')->getStorage('taxonomy_term')
77 public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
78 parent::init($view, $display, $options);
80 if (!empty($this->definition['vocabulary'])) {
81 $this->options['vid'] = $this->definition['vocabulary'];
85 public function hasExtraOptions() {
92 public function getValueOptions() {
93 return $this->valueOptions;
96 protected function defineOptions() {
97 $options = parent::defineOptions();
99 $options['type'] = ['default' => 'textfield'];
100 $options['limit'] = ['default' => TRUE];
101 $options['vid'] = ['default' => ''];
102 $options['hierarchy'] = ['default' => FALSE];
103 $options['error_message'] = ['default' => TRUE];
108 public function buildExtraOptionsForm(&$form, FormStateInterface $form_state) {
109 $vocabularies = $this->vocabularyStorage->loadMultiple();
111 foreach ($vocabularies as $voc) {
112 $options[$voc->id()] = $voc->label();
115 if ($this->options['limit']) {
116 // We only do this when the form is displayed.
117 if (empty($this->options['vid'])) {
118 $first_vocabulary = reset($vocabularies);
119 $this->options['vid'] = $first_vocabulary->id();
122 if (empty($this->definition['vocabulary'])) {
125 '#title' => $this->t('Vocabulary'),
126 '#options' => $options,
127 '#description' => $this->t('Select which vocabulary to show terms for in the regular options.'),
128 '#default_value' => $this->options['vid'],
135 '#title' => $this->t('Selection type'),
136 '#options' => ['select' => $this->t('Dropdown'), 'textfield' => $this->t('Autocomplete')],
137 '#default_value' => $this->options['type'],
140 $form['hierarchy'] = [
141 '#type' => 'checkbox',
142 '#title' => $this->t('Show hierarchy in dropdown'),
143 '#default_value' => !empty($this->options['hierarchy']),
146 ':input[name="options[type]"]' => ['value' => 'select'],
152 protected function valueForm(&$form, FormStateInterface $form_state) {
153 $vocabulary = $this->vocabularyStorage->load($this->options['vid']);
154 if (empty($vocabulary) && $this->options['limit']) {
156 '#markup' => '<div class="js-form-item form-item">' . $this->t('An invalid vocabulary is selected. Please change it in the options.') . '</div>',
161 if ($this->options['type'] == 'textfield') {
162 $terms = $this->value ? Term::loadMultiple(($this->value)) : [];
164 '#title' => $this->options['limit'] ? $this->t('Select terms from vocabulary @voc', ['@voc' => $vocabulary->label()]) : $this->t('Select terms'),
165 '#type' => 'textfield',
166 '#default_value' => EntityAutocomplete::getEntityLabels($terms),
169 if ($this->options['limit']) {
170 $form['value']['#type'] = 'entity_autocomplete';
171 $form['value']['#target_type'] = 'taxonomy_term';
172 $form['value']['#selection_settings']['target_bundles'] = [$vocabulary->id()];
173 $form['value']['#tags'] = TRUE;
174 $form['value']['#process_default_value'] = FALSE;
178 if (!empty($this->options['hierarchy']) && $this->options['limit']) {
179 $tree = $this->termStorage->loadTree($vocabulary->id(), 0, NULL, TRUE);
183 foreach ($tree as $term) {
184 $choice = new \stdClass();
185 $choice->option = [$term->id() => str_repeat('-', $term->depth) . \Drupal::entityManager()->getTranslationFromContext($term)->label()];
186 $options[] = $choice;
192 $query = \Drupal::entityQuery('taxonomy_term')
193 // @todo Sorting on vocabulary properties -
194 // https://www.drupal.org/node/1821274.
197 ->addTag('taxonomy_term_access');
198 if ($this->options['limit']) {
199 $query->condition('vid', $vocabulary->id());
201 $terms = Term::loadMultiple($query->execute());
202 foreach ($terms as $term) {
203 $options[$term->id()] = \Drupal::entityManager()->getTranslationFromContext($term)->label();
207 $default_value = (array) $this->value;
209 if ($exposed = $form_state->get('exposed')) {
210 $identifier = $this->options['expose']['identifier'];
212 if (!empty($this->options['expose']['reduce'])) {
213 $options = $this->reduceValueOptions($options);
215 if (!empty($this->options['expose']['multiple']) && empty($this->options['expose']['required'])) {
220 if (empty($this->options['expose']['multiple'])) {
221 if (empty($this->options['expose']['required']) && (empty($default_value) || !empty($this->options['expose']['reduce']))) {
222 $default_value = 'All';
224 elseif (empty($default_value)) {
225 $keys = array_keys($options);
226 $default_value = array_shift($keys);
228 // Due to #1464174 there is a chance that array('') was saved in the admin ui.
229 // Let's choose a safe default value.
230 elseif ($default_value == ['']) {
231 $default_value = 'All';
234 $copy = $default_value;
235 $default_value = array_shift($copy);
241 '#title' => $this->options['limit'] ? $this->t('Select terms from vocabulary @voc', ['@voc' => $vocabulary->label()]) : $this->t('Select terms'),
243 '#options' => $options,
244 '#size' => min(9, count($options)),
245 '#default_value' => $default_value,
248 $user_input = $form_state->getUserInput();
249 if ($exposed && isset($identifier) && !isset($user_input[$identifier])) {
250 $user_input[$identifier] = $default_value;
251 $form_state->setUserInput($user_input);
255 if (!$form_state->get('exposed')) {
256 // Retain the helper option
257 $this->helper->buildOptionsForm($form, $form_state);
259 // Show help text if not exposed to end users.
260 $form['value']['#description'] = t('Leave blank for all. Otherwise, the first selected term will be the default instead of "Any".');
264 protected function valueValidate($form, FormStateInterface $form_state) {
265 // We only validate if they've chosen the text field style.
266 if ($this->options['type'] != 'textfield') {
271 if ($values = $form_state->getValue(['options', 'value'])) {
272 foreach ($values as $value) {
273 $tids[] = $value['target_id'];
276 $form_state->setValue(['options', 'value'], $tids);
279 public function acceptExposedInput($input) {
280 if (empty($this->options['exposed'])) {
283 // We need to know the operator, which is normally set in
284 // \Drupal\views\Plugin\views\filter\FilterPluginBase::acceptExposedInput(),
285 // before we actually call the parent version of ourselves.
286 if (!empty($this->options['expose']['use_operator']) && !empty($this->options['expose']['operator_id']) && isset($input[$this->options['expose']['operator_id']])) {
287 $this->operator = $input[$this->options['expose']['operator_id']];
290 // If view is an attachment and is inheriting exposed filters, then assume
291 // exposed input has already been validated
292 if (!empty($this->view->is_attachment) && $this->view->display_handler->usesExposed()) {
293 $this->validated_exposed_input = (array) $this->view->exposed_raw_input[$this->options['expose']['identifier']];
296 // If we're checking for EMPTY or NOT, we don't need any input, and we can
297 // say that our input conditions are met by just having the right operator.
298 if ($this->operator == 'empty' || $this->operator == 'not empty') {
302 // If it's non-required and there's no value don't bother filtering.
303 if (!$this->options['expose']['required'] && empty($this->validated_exposed_input)) {
307 $rc = parent::acceptExposedInput($input);
309 // If we have previously validated input, override.
310 if (isset($this->validated_exposed_input)) {
311 $this->value = $this->validated_exposed_input;
318 public function validateExposed(&$form, FormStateInterface $form_state) {
319 if (empty($this->options['exposed'])) {
323 $identifier = $this->options['expose']['identifier'];
325 // We only validate if they've chosen the text field style.
326 if ($this->options['type'] != 'textfield') {
327 if ($form_state->getValue($identifier) != 'All') {
328 $this->validated_exposed_input = (array) $form_state->getValue($identifier);
333 if (empty($this->options['expose']['identifier'])) {
337 if ($values = $form_state->getValue($identifier)) {
338 foreach ($values as $value) {
339 $this->validated_exposed_input[] = $value['target_id'];
344 protected function valueSubmit($form, FormStateInterface $form_state) {
345 // prevent array_filter from messing up our arrays in parent submit.
348 public function buildExposeForm(&$form, FormStateInterface $form_state) {
349 parent::buildExposeForm($form, $form_state);
350 if ($this->options['type'] != 'select') {
351 unset($form['expose']['reduce']);
353 $form['error_message'] = [
354 '#type' => 'checkbox',
355 '#title' => $this->t('Display error message'),
356 '#default_value' => !empty($this->options['error_message']),
360 public function adminSummary() {
361 // set up $this->valueOptions for the parent summary
362 $this->valueOptions = [];
365 $this->value = array_filter($this->value);
366 $terms = Term::loadMultiple($this->value);
367 foreach ($terms as $term) {
368 $this->valueOptions[$term->id()] = \Drupal::entityManager()->getTranslationFromContext($term)->label();
371 return parent::adminSummary();
377 public function getCacheContexts() {
378 $contexts = parent::getCacheContexts();
379 // The result potentially depends on term access and so is just cacheable
381 // @todo See https://www.drupal.org/node/2352175.
382 $contexts[] = 'user';
390 public function calculateDependencies() {
391 $dependencies = parent::calculateDependencies();
393 $vocabulary = $this->vocabularyStorage->load($this->options['vid']);
394 $dependencies[$vocabulary->getConfigDependencyKey()][] = $vocabulary->getConfigDependencyName();
396 foreach ($this->termStorage->loadMultiple($this->options['value']) as $term) {
397 $dependencies[$term->getConfigDependencyKey()][] = $term->getConfigDependencyName();
400 return $dependencies;