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() { return TRUE; }
90 public function getValueOptions() {
91 return $this->valueOptions;
94 protected function defineOptions() {
95 $options = parent::defineOptions();
97 $options['type'] = ['default' => 'textfield'];
98 $options['limit'] = ['default' => TRUE];
99 $options['vid'] = ['default' => ''];
100 $options['hierarchy'] = ['default' => FALSE];
101 $options['error_message'] = ['default' => TRUE];
106 public function buildExtraOptionsForm(&$form, FormStateInterface $form_state) {
107 $vocabularies = $this->vocabularyStorage->loadMultiple();
109 foreach ($vocabularies as $voc) {
110 $options[$voc->id()] = $voc->label();
113 if ($this->options['limit']) {
114 // We only do this when the form is displayed.
115 if (empty($this->options['vid'])) {
116 $first_vocabulary = reset($vocabularies);
117 $this->options['vid'] = $first_vocabulary->id();
120 if (empty($this->definition['vocabulary'])) {
123 '#title' => $this->t('Vocabulary'),
124 '#options' => $options,
125 '#description' => $this->t('Select which vocabulary to show terms for in the regular options.'),
126 '#default_value' => $this->options['vid'],
133 '#title' => $this->t('Selection type'),
134 '#options' => ['select' => $this->t('Dropdown'), 'textfield' => $this->t('Autocomplete')],
135 '#default_value' => $this->options['type'],
138 $form['hierarchy'] = [
139 '#type' => 'checkbox',
140 '#title' => $this->t('Show hierarchy in dropdown'),
141 '#default_value' => !empty($this->options['hierarchy']),
144 ':input[name="options[type]"]' => ['value' => 'select'],
150 protected function valueForm(&$form, FormStateInterface $form_state) {
151 $vocabulary = $this->vocabularyStorage->load($this->options['vid']);
152 if (empty($vocabulary) && $this->options['limit']) {
154 '#markup' => '<div class="js-form-item form-item">' . $this->t('An invalid vocabulary is selected. Please change it in the options.') . '</div>',
159 if ($this->options['type'] == 'textfield') {
160 $terms = $this->value ? Term::loadMultiple(($this->value)) : [];
162 '#title' => $this->options['limit'] ? $this->t('Select terms from vocabulary @voc', ['@voc' => $vocabulary->label()]) : $this->t('Select terms'),
163 '#type' => 'textfield',
164 '#default_value' => EntityAutocomplete::getEntityLabels($terms),
167 if ($this->options['limit']) {
168 $form['value']['#type'] = 'entity_autocomplete';
169 $form['value']['#target_type'] = 'taxonomy_term';
170 $form['value']['#selection_settings']['target_bundles'] = [$vocabulary->id()];
171 $form['value']['#tags'] = TRUE;
172 $form['value']['#process_default_value'] = FALSE;
176 if (!empty($this->options['hierarchy']) && $this->options['limit']) {
177 $tree = $this->termStorage->loadTree($vocabulary->id(), 0, NULL, TRUE);
181 foreach ($tree as $term) {
182 $choice = new \stdClass();
183 $choice->option = [$term->id() => str_repeat('-', $term->depth) . \Drupal::entityManager()->getTranslationFromContext($term)->label()];
184 $options[] = $choice;
190 $query = \Drupal::entityQuery('taxonomy_term')
191 // @todo Sorting on vocabulary properties -
192 // https://www.drupal.org/node/1821274.
195 ->addTag('taxonomy_term_access');
196 if ($this->options['limit']) {
197 $query->condition('vid', $vocabulary->id());
199 $terms = Term::loadMultiple($query->execute());
200 foreach ($terms as $term) {
201 $options[$term->id()] = \Drupal::entityManager()->getTranslationFromContext($term)->label();
205 $default_value = (array) $this->value;
207 if ($exposed = $form_state->get('exposed')) {
208 $identifier = $this->options['expose']['identifier'];
210 if (!empty($this->options['expose']['reduce'])) {
211 $options = $this->reduceValueOptions($options);
213 if (!empty($this->options['expose']['multiple']) && empty($this->options['expose']['required'])) {
218 if (empty($this->options['expose']['multiple'])) {
219 if (empty($this->options['expose']['required']) && (empty($default_value) || !empty($this->options['expose']['reduce']))) {
220 $default_value = 'All';
222 elseif (empty($default_value)) {
223 $keys = array_keys($options);
224 $default_value = array_shift($keys);
226 // Due to #1464174 there is a chance that array('') was saved in the admin ui.
227 // Let's choose a safe default value.
228 elseif ($default_value == ['']) {
229 $default_value = 'All';
232 $copy = $default_value;
233 $default_value = array_shift($copy);
239 '#title' => $this->options['limit'] ? $this->t('Select terms from vocabulary @voc', ['@voc' => $vocabulary->label()]) : $this->t('Select terms'),
241 '#options' => $options,
242 '#size' => min(9, count($options)),
243 '#default_value' => $default_value,
246 $user_input = $form_state->getUserInput();
247 if ($exposed && isset($identifier) && !isset($user_input[$identifier])) {
248 $user_input[$identifier] = $default_value;
249 $form_state->setUserInput($user_input);
253 if (!$form_state->get('exposed')) {
254 // Retain the helper option
255 $this->helper->buildOptionsForm($form, $form_state);
257 // Show help text if not exposed to end users.
258 $form['value']['#description'] = t('Leave blank for all. Otherwise, the first selected term will be the default instead of "Any".');
262 protected function valueValidate($form, FormStateInterface $form_state) {
263 // We only validate if they've chosen the text field style.
264 if ($this->options['type'] != 'textfield') {
269 if ($values = $form_state->getValue(['options', 'value'])) {
270 foreach ($values as $value) {
271 $tids[] = $value['target_id'];
274 $form_state->setValue(['options', 'value'], $tids);
277 public function acceptExposedInput($input) {
278 if (empty($this->options['exposed'])) {
281 // We need to know the operator, which is normally set in
282 // \Drupal\views\Plugin\views\filter\FilterPluginBase::acceptExposedInput(),
283 // before we actually call the parent version of ourselves.
284 if (!empty($this->options['expose']['use_operator']) && !empty($this->options['expose']['operator_id']) && isset($input[$this->options['expose']['operator_id']])) {
285 $this->operator = $input[$this->options['expose']['operator_id']];
288 // If view is an attachment and is inheriting exposed filters, then assume
289 // exposed input has already been validated
290 if (!empty($this->view->is_attachment) && $this->view->display_handler->usesExposed()) {
291 $this->validated_exposed_input = (array) $this->view->exposed_raw_input[$this->options['expose']['identifier']];
294 // If we're checking for EMPTY or NOT, we don't need any input, and we can
295 // say that our input conditions are met by just having the right operator.
296 if ($this->operator == 'empty' || $this->operator == 'not empty') {
300 // If it's non-required and there's no value don't bother filtering.
301 if (!$this->options['expose']['required'] && empty($this->validated_exposed_input)) {
305 $rc = parent::acceptExposedInput($input);
307 // If we have previously validated input, override.
308 if (isset($this->validated_exposed_input)) {
309 $this->value = $this->validated_exposed_input;
316 public function validateExposed(&$form, FormStateInterface $form_state) {
317 if (empty($this->options['exposed'])) {
321 $identifier = $this->options['expose']['identifier'];
323 // We only validate if they've chosen the text field style.
324 if ($this->options['type'] != 'textfield') {
325 if ($form_state->getValue($identifier) != 'All') {
326 $this->validated_exposed_input = (array) $form_state->getValue($identifier);
331 if (empty($this->options['expose']['identifier'])) {
335 if ($values = $form_state->getValue($identifier)) {
336 foreach ($values as $value) {
337 $this->validated_exposed_input[] = $value['target_id'];
342 protected function valueSubmit($form, FormStateInterface $form_state) {
343 // prevent array_filter from messing up our arrays in parent submit.
346 public function buildExposeForm(&$form, FormStateInterface $form_state) {
347 parent::buildExposeForm($form, $form_state);
348 if ($this->options['type'] != 'select') {
349 unset($form['expose']['reduce']);
351 $form['error_message'] = [
352 '#type' => 'checkbox',
353 '#title' => $this->t('Display error message'),
354 '#default_value' => !empty($this->options['error_message']),
358 public function adminSummary() {
359 // set up $this->valueOptions for the parent summary
360 $this->valueOptions = [];
363 $this->value = array_filter($this->value);
364 $terms = Term::loadMultiple($this->value);
365 foreach ($terms as $term) {
366 $this->valueOptions[$term->id()] = \Drupal::entityManager()->getTranslationFromContext($term)->label();
369 return parent::adminSummary();
375 public function getCacheContexts() {
376 $contexts = parent::getCacheContexts();
377 // The result potentially depends on term access and so is just cacheable
379 // @todo See https://www.drupal.org/node/2352175.
380 $contexts[] = 'user';
388 public function calculateDependencies() {
389 $dependencies = parent::calculateDependencies();
391 $vocabulary = $this->vocabularyStorage->load($this->options['vid']);
392 $dependencies[$vocabulary->getConfigDependencyKey()][] = $vocabulary->getConfigDependencyName();
394 foreach ($this->termStorage->loadMultiple($this->options['value']) as $term) {
395 $dependencies[$term->getConfigDependencyKey()][] = $term->getConfigDependencyName();
398 return $dependencies;