Pull merge.
[yaffs-website] / web / core / modules / views / src / Plugin / views / field / FieldPluginBase.php
1 <?php
2
3 namespace Drupal\views\Plugin\views\field;
4
5 use Drupal\Component\Utility\Html;
6 use Drupal\Component\Render\MarkupInterface;
7 use Drupal\Component\Utility\UrlHelper;
8 use Drupal\Component\Utility\Xss;
9 use Drupal\Core\Form\FormStateInterface;
10 use Drupal\Core\Url as CoreUrl;
11 use Drupal\views\Plugin\views\HandlerBase;
12 use Drupal\views\Plugin\views\display\DisplayPluginBase;
13 use Drupal\views\Render\ViewsRenderPipelineMarkup;
14 use Drupal\views\ResultRow;
15 use Drupal\views\ViewExecutable;
16
17 /**
18  * @defgroup views_field_handlers Views field handler plugins
19  * @{
20  * Handler plugins for Views fields.
21  *
22  * Field handlers handle both querying and display of fields in views.
23  *
24  * Field handler plugins extend
25  * \Drupal\views\Plugin\views\field\FieldPluginBase. They must be
26  * annotated with \Drupal\views\Annotation\ViewsField annotation, and they
27  * must be in namespace directory Plugin\views\field.
28  *
29  * The following items can go into a hook_views_data() implementation in a
30  * field section to affect how the field handler will behave:
31  * - additional fields: An array of fields that should be added to the query.
32  *   The array is in one of these forms:
33  *   @code
34  *   // Simple form, for fields within the same table.
35  *   array('identifier' => fieldname)
36  *   // Form for fields in a different table.
37  *   array('identifier' => array('table' => tablename, 'field' => fieldname))
38  *   @endcode
39  *   As many fields as are necessary may be in this array.
40  * - click sortable: If TRUE (default), this field may be click sorted.
41  *
42  * @ingroup views_plugins
43  * @see plugin_api
44  */
45
46 /**
47  * Base class for views fields.
48  *
49  * @ingroup views_field_handlers
50  */
51 abstract class FieldPluginBase extends HandlerBase implements FieldHandlerInterface {
52
53   /**
54    * Indicator of the renderText() method for rendering a single item.
55    * (If no render_item() is present).
56    */
57   const RENDER_TEXT_PHASE_SINGLE_ITEM = 0;
58
59   /**
60    * Indicator of the renderText() method for rendering the whole element.
61    * (if no render_item() method is available).
62    */
63   const RENDER_TEXT_PHASE_COMPLETELY = 1;
64
65   /**
66    * Indicator of the renderText() method for rendering the empty text.
67    */
68   const RENDER_TEXT_PHASE_EMPTY = 2;
69
70   /**
71    * @var string
72    */
73   public $field_alias = 'unknown';
74   public $aliases = [];
75
76   /**
77    * The field value prior to any rewriting.
78    *
79    * @var mixed
80    */
81   public $original_value = NULL;
82
83   /**
84    * Stores additional fields which get added to the query.
85    *
86    * The generated aliases are stored in $aliases.
87    *
88    * @var array
89    */
90   public $additional_fields = [];
91
92   /**
93    * The link generator.
94    *
95    * @var \Drupal\Core\Utility\LinkGeneratorInterface
96    */
97   protected $linkGenerator;
98
99   /**
100    * Stores the render API renderer.
101    *
102    * @var \Drupal\Core\Render\RendererInterface
103    */
104   protected $renderer;
105
106   /**
107    * Keeps track of the last render index.
108    *
109    * @var int|null
110    */
111   protected $lastRenderIndex;
112
113   /**
114    * {@inheritdoc}
115    */
116   public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
117     parent::init($view, $display, $options);
118
119     $this->additional_fields = [];
120     if (!empty($this->definition['additional fields'])) {
121       $this->additional_fields = $this->definition['additional fields'];
122     }
123
124     if (!isset($this->options['exclude'])) {
125       $this->options['exclude'] = '';
126     }
127   }
128
129   /**
130    * Determine if this field can allow advanced rendering.
131    *
132    * Fields can set this to FALSE if they do not wish to allow
133    * token based rewriting or link-making.
134    */
135   protected function allowAdvancedRender() {
136     return TRUE;
137   }
138
139   /**
140    * Called to add the field to a query.
141    */
142   public function query() {
143     $this->ensureMyTable();
144     // Add the field.
145     $params = $this->options['group_type'] != 'group' ? ['function' => $this->options['group_type']] : [];
146     $this->field_alias = $this->query->addField($this->tableAlias, $this->realField, NULL, $params);
147
148     $this->addAdditionalFields();
149   }
150
151   /**
152    * Add 'additional' fields to the query.
153    *
154    * @param $fields
155    *   An array of fields. The key is an identifier used to later find the
156    *   field alias used. The value is either a string in which case it's
157    *   assumed to be a field on this handler's table; or it's an array in the
158    *   form of
159    *   @code array('table' => $tablename, 'field' => $fieldname) @endcode
160    */
161   protected function addAdditionalFields($fields = NULL) {
162     if (!isset($fields)) {
163       // notice check
164       if (empty($this->additional_fields)) {
165         return;
166       }
167       $fields = $this->additional_fields;
168     }
169
170     $group_params = [];
171     if ($this->options['group_type'] != 'group') {
172       $group_params = [
173         'function' => $this->options['group_type'],
174       ];
175     }
176
177     if (!empty($fields) && is_array($fields)) {
178       foreach ($fields as $identifier => $info) {
179         if (is_array($info)) {
180           if (isset($info['table'])) {
181             $table_alias = $this->query->ensureTable($info['table'], $this->relationship);
182           }
183           else {
184             $table_alias = $this->tableAlias;
185           }
186
187           if (empty($table_alias)) {
188             trigger_error(sprintf(
189               "Handler % tried to add additional_field %s but % could not be added!",
190               $this->definition['id'],
191               $identifier,
192               $info['table']
193             ), E_USER_WARNING);
194
195             $this->aliases[$identifier] = 'broken';
196             continue;
197           }
198
199           $params = [];
200           if (!empty($info['params'])) {
201             $params = $info['params'];
202           }
203
204           $params += $group_params;
205           $this->aliases[$identifier] = $this->query->addField($table_alias, $info['field'], NULL, $params);
206         }
207         else {
208           $this->aliases[$info] = $this->query->addField($this->tableAlias, $info, NULL, $group_params);
209         }
210       }
211     }
212   }
213
214   /**
215    * {@inheritdoc}
216    */
217   public function clickSort($order) {
218     if (isset($this->field_alias)) {
219       // Since fields should always have themselves already added, just
220       // add a sort on the field.
221       $params = $this->options['group_type'] != 'group' ? ['function' => $this->options['group_type']] : [];
222       $this->query->addOrderBy(NULL, NULL, $order, $this->field_alias, $params);
223     }
224   }
225
226   /**
227    * {@inheritdoc}
228    */
229   public function clickSortable() {
230     return isset($this->definition['click sortable']) ? $this->definition['click sortable'] : TRUE;
231   }
232
233   /**
234    * {@inheritdoc}
235    */
236   public function label() {
237     if (!isset($this->options['label'])) {
238       return '';
239     }
240     return $this->options['label'];
241   }
242
243   /**
244    * {@inheritdoc}
245    */
246   public function elementType($none_supported = FALSE, $default_empty = FALSE, $inline = FALSE) {
247     if ($none_supported) {
248       if ($this->options['element_type'] === '0') {
249         return '';
250       }
251     }
252     if ($this->options['element_type']) {
253       return $this->options['element_type'];
254     }
255
256     if ($default_empty) {
257       return '';
258     }
259
260     if ($inline) {
261       return 'span';
262     }
263
264     if (isset($this->definition['element type'])) {
265       return $this->definition['element type'];
266     }
267
268     return 'span';
269   }
270
271   /**
272    * {@inheritdoc}
273    */
274   public function elementLabelType($none_supported = FALSE, $default_empty = FALSE) {
275     if ($none_supported) {
276       if ($this->options['element_label_type'] === '0') {
277         return '';
278       }
279     }
280     if ($this->options['element_label_type']) {
281       return $this->options['element_label_type'];
282     }
283
284     if ($default_empty) {
285       return '';
286     }
287
288     return 'span';
289   }
290
291   /**
292    * {@inheritdoc}
293    */
294   public function elementWrapperType($none_supported = FALSE, $default_empty = FALSE) {
295     if ($none_supported) {
296       if ($this->options['element_wrapper_type'] === '0') {
297         return 0;
298       }
299     }
300     if ($this->options['element_wrapper_type']) {
301       return $this->options['element_wrapper_type'];
302     }
303
304     if ($default_empty) {
305       return '';
306     }
307
308     return 'div';
309   }
310
311   /**
312    * {@inheritdoc}
313    */
314   public function getElements() {
315     static $elements = NULL;
316     if (!isset($elements)) {
317       // @todo Add possible html5 elements.
318       $elements = [
319         '' => $this->t('- Use default -'),
320         '0' => $this->t('- None -'),
321       ];
322       $elements += \Drupal::config('views.settings')->get('field_rewrite_elements');
323     }
324
325     return $elements;
326   }
327
328   /**
329    * {@inheritdoc}
330    */
331   public function elementClasses($row_index = NULL) {
332     $classes = $this->tokenizeValue($this->options['element_class'], $row_index);
333     $classes = explode(' ', $classes);
334     foreach ($classes as &$class) {
335       $class = Html::cleanCssIdentifier($class);
336     }
337     return implode(' ', $classes);
338   }
339
340   /**
341    * {@inheritdoc}
342    */
343   public function tokenizeValue($value, $row_index = NULL) {
344     if (strpos($value, '{{') !== FALSE) {
345       $fake_item = [
346         'alter_text' => TRUE,
347         'text' => $value,
348       ];
349
350       // Use isset() because empty() will trigger on 0 and 0 is
351       // the first row.
352       if (isset($row_index) && isset($this->view->style_plugin->render_tokens[$row_index])) {
353         $tokens = $this->view->style_plugin->render_tokens[$row_index];
354       }
355       else {
356         // Get tokens from the last field.
357         $last_field = end($this->view->field);
358         if (isset($last_field->last_tokens)) {
359           $tokens = $last_field->last_tokens;
360         }
361         else {
362           $tokens = $last_field->getRenderTokens($fake_item);
363         }
364       }
365
366       $value = strip_tags($this->renderAltered($fake_item, $tokens));
367       if (!empty($this->options['alter']['trim_whitespace'])) {
368         $value = trim($value);
369       }
370     }
371
372     return $value;
373   }
374
375   /**
376    * {@inheritdoc}
377    */
378   public function elementLabelClasses($row_index = NULL) {
379     $classes = $this->tokenizeValue($this->options['element_label_class'], $row_index);
380     $classes = explode(' ', $classes);
381     foreach ($classes as &$class) {
382       $class = Html::cleanCssIdentifier($class);
383     }
384     return implode(' ', $classes);
385   }
386
387   /**
388    * {@inheritdoc}
389    */
390   public function elementWrapperClasses($row_index = NULL) {
391     $classes = $this->tokenizeValue($this->options['element_wrapper_class'], $row_index);
392     $classes = explode(' ', $classes);
393     foreach ($classes as &$class) {
394       $class = Html::cleanCssIdentifier($class);
395     }
396     return implode(' ', $classes);
397   }
398
399   /**
400    * {@inheritdoc}
401    */
402   public function getEntity(ResultRow $values) {
403     $relationship_id = $this->options['relationship'];
404     if ($relationship_id == 'none') {
405       return $values->_entity;
406     }
407     elseif (isset($values->_relationship_entities[$relationship_id])) {
408       return $values->_relationship_entities[$relationship_id];
409     }
410   }
411
412   /**
413    * {@inheritdoc}
414    */
415   public function getValue(ResultRow $values, $field = NULL) {
416     $alias = isset($field) ? $this->aliases[$field] : $this->field_alias;
417     if (isset($values->{$alias})) {
418       return $values->{$alias};
419     }
420   }
421
422   /**
423    * {@inheritdoc}
424    */
425   public function useStringGroupBy() {
426     return TRUE;
427   }
428
429   protected function defineOptions() {
430     $options = parent::defineOptions();
431
432     $options['label'] = ['default' => ''];
433     // Some styles (for example table) should have labels enabled by default.
434     $style = $this->view->getStyle();
435     if (isset($style) && $style->defaultFieldLabels()) {
436       $options['label']['default'] = $this->definition['title'];
437     }
438
439     $options['exclude'] = ['default' => FALSE];
440     $options['alter'] = [
441       'contains' => [
442         'alter_text' => ['default' => FALSE],
443         'text' => ['default' => ''],
444         'make_link' => ['default' => FALSE],
445         'path' => ['default' => ''],
446         'absolute' => ['default' => FALSE],
447         'external' => ['default' => FALSE],
448         'replace_spaces' => ['default' => FALSE],
449         'path_case' => ['default' => 'none'],
450         'trim_whitespace' => ['default' => FALSE],
451         'alt' => ['default' => ''],
452         'rel' => ['default' => ''],
453         'link_class' => ['default' => ''],
454         'prefix' => ['default' => ''],
455         'suffix' => ['default' => ''],
456         'target' => ['default' => ''],
457         'nl2br' => ['default' => FALSE],
458         'max_length' => ['default' => 0],
459         'word_boundary' => ['default' => TRUE],
460         'ellipsis' => ['default' => TRUE],
461         'more_link' => ['default' => FALSE],
462         'more_link_text' => ['default' => ''],
463         'more_link_path' => ['default' => ''],
464         'strip_tags' => ['default' => FALSE],
465         'trim' => ['default' => FALSE],
466         'preserve_tags' => ['default' => ''],
467         'html' => ['default' => FALSE],
468       ],
469     ];
470     $options['element_type'] = ['default' => ''];
471     $options['element_class'] = ['default' => ''];
472
473     $options['element_label_type'] = ['default' => ''];
474     $options['element_label_class'] = ['default' => ''];
475     $options['element_label_colon'] = ['default' => TRUE];
476
477     $options['element_wrapper_type'] = ['default' => ''];
478     $options['element_wrapper_class'] = ['default' => ''];
479
480     $options['element_default_classes'] = ['default' => TRUE];
481
482     $options['empty'] = ['default' => ''];
483     $options['hide_empty'] = ['default' => FALSE];
484     $options['empty_zero'] = ['default' => FALSE];
485     $options['hide_alter_empty'] = ['default' => TRUE];
486
487     return $options;
488   }
489
490   /**
491    * Performs some cleanup tasks on the options array before saving it.
492    */
493   public function submitOptionsForm(&$form, FormStateInterface $form_state) {
494     $options = &$form_state->getValue('options');
495     $types = ['element_type', 'element_label_type', 'element_wrapper_type'];
496     $classes = array_combine(['element_class', 'element_label_class', 'element_wrapper_class'], $types);
497
498     foreach ($types as $type) {
499       if (!$options[$type . '_enable']) {
500         $options[$type] = '';
501       }
502     }
503
504     foreach ($classes as $class => $type) {
505       if (!$options[$class . '_enable'] || !$options[$type . '_enable']) {
506         $options[$class] = '';
507       }
508     }
509
510     if (empty($options['custom_label'])) {
511       $options['label'] = '';
512       $options['element_label_colon'] = FALSE;
513     }
514   }
515
516   /**
517    * Default options form that provides the label widget that all fields
518    * should have.
519    */
520   public function buildOptionsForm(&$form, FormStateInterface $form_state) {
521     parent::buildOptionsForm($form, $form_state);
522
523     $label = $this->label();
524     $form['custom_label'] = [
525       '#type' => 'checkbox',
526       '#title' => $this->t('Create a label'),
527       '#default_value' => $label !== '',
528       '#weight' => -103,
529     ];
530     $form['label'] = [
531       '#type' => 'textfield',
532       '#title' => $this->t('Label'),
533       '#default_value' => $label,
534       '#states' => [
535         'visible' => [
536           ':input[name="options[custom_label]"]' => ['checked' => TRUE],
537         ],
538       ],
539       '#weight' => -102,
540     ];
541     $form['element_label_colon'] = [
542       '#type' => 'checkbox',
543       '#title' => $this->t('Place a colon after the label'),
544       '#default_value' => $this->options['element_label_colon'],
545       '#states' => [
546         'visible' => [
547           ':input[name="options[custom_label]"]' => ['checked' => TRUE],
548         ],
549       ],
550       '#weight' => -101,
551     ];
552
553     $form['exclude'] = [
554       '#type' => 'checkbox',
555       '#title' => $this->t('Exclude from display'),
556       '#default_value' => $this->options['exclude'],
557       '#description' => $this->t('Enable to load this field as hidden. Often used to group fields, or to use as token in another field.'),
558       '#weight' => -100,
559     ];
560
561     $form['style_settings'] = [
562       '#type' => 'details',
563       '#title' => $this->t('Style settings'),
564       '#weight' => 99,
565     ];
566
567     $form['element_type_enable'] = [
568       '#type' => 'checkbox',
569       '#title' => $this->t('Customize field HTML'),
570       '#default_value' => !empty($this->options['element_type']) || (string) $this->options['element_type'] == '0' || !empty($this->options['element_class']) || (string) $this->options['element_class'] == '0',
571       '#fieldset' => 'style_settings',
572     ];
573     $form['element_type'] = [
574       '#title' => $this->t('HTML element'),
575       '#options' => $this->getElements(),
576       '#type' => 'select',
577       '#default_value' => $this->options['element_type'],
578       '#description' => $this->t('Choose the HTML element to wrap around this field, e.g. H1, H2, etc.'),
579       '#states' => [
580         'visible' => [
581           ':input[name="options[element_type_enable]"]' => ['checked' => TRUE],
582         ],
583       ],
584       '#fieldset' => 'style_settings',
585     ];
586
587     $form['element_class_enable'] = [
588       '#type' => 'checkbox',
589       '#title' => $this->t('Create a CSS class'),
590       '#states' => [
591         'visible' => [
592           ':input[name="options[element_type_enable]"]' => ['checked' => TRUE],
593         ],
594       ],
595       '#default_value' => !empty($this->options['element_class']) || (string) $this->options['element_class'] == '0',
596       '#fieldset' => 'style_settings',
597     ];
598     $form['element_class'] = [
599       '#title' => $this->t('CSS class'),
600       '#description' => $this->t('You may use token substitutions from the rewriting section in this class.'),
601       '#type' => 'textfield',
602       '#default_value' => $this->options['element_class'],
603       '#states' => [
604         'visible' => [
605           ':input[name="options[element_type_enable]"]' => ['checked' => TRUE],
606           ':input[name="options[element_class_enable]"]' => ['checked' => TRUE],
607         ],
608       ],
609       '#fieldset' => 'style_settings',
610     ];
611
612     $form['element_label_type_enable'] = [
613       '#type' => 'checkbox',
614       '#title' => $this->t('Customize label HTML'),
615       '#default_value' => !empty($this->options['element_label_type']) || (string) $this->options['element_label_type'] == '0' || !empty($this->options['element_label_class']) || (string) $this->options['element_label_class'] == '0',
616       '#fieldset' => 'style_settings',
617     ];
618     $form['element_label_type'] = [
619       '#title' => $this->t('Label HTML element'),
620       '#options' => $this->getElements(FALSE),
621       '#type' => 'select',
622       '#default_value' => $this->options['element_label_type'],
623       '#description' => $this->t('Choose the HTML element to wrap around this label, e.g. H1, H2, etc.'),
624       '#states' => [
625         'visible' => [
626           ':input[name="options[element_label_type_enable]"]' => ['checked' => TRUE],
627         ],
628       ],
629       '#fieldset' => 'style_settings',
630     ];
631     $form['element_label_class_enable'] = [
632       '#type' => 'checkbox',
633       '#title' => $this->t('Create a CSS class'),
634       '#states' => [
635         'visible' => [
636           ':input[name="options[element_label_type_enable]"]' => ['checked' => TRUE],
637         ],
638       ],
639       '#default_value' => !empty($this->options['element_label_class']) || (string) $this->options['element_label_class'] == '0',
640       '#fieldset' => 'style_settings',
641     ];
642     $form['element_label_class'] = [
643       '#title' => $this->t('CSS class'),
644       '#description' => $this->t('You may use token substitutions from the rewriting section in this class.'),
645       '#type' => 'textfield',
646       '#default_value' => $this->options['element_label_class'],
647       '#states' => [
648         'visible' => [
649           ':input[name="options[element_label_type_enable]"]' => ['checked' => TRUE],
650           ':input[name="options[element_label_class_enable]"]' => ['checked' => TRUE],
651         ],
652       ],
653       '#fieldset' => 'style_settings',
654     ];
655
656     $form['element_wrapper_type_enable'] = [
657       '#type' => 'checkbox',
658       '#title' => $this->t('Customize field and label wrapper HTML'),
659       '#default_value' => !empty($this->options['element_wrapper_type']) || (string) $this->options['element_wrapper_type'] == '0' || !empty($this->options['element_wrapper_class']) || (string) $this->options['element_wrapper_class'] == '0',
660       '#fieldset' => 'style_settings',
661     ];
662     $form['element_wrapper_type'] = [
663       '#title' => $this->t('Wrapper HTML element'),
664       '#options' => $this->getElements(FALSE),
665       '#type' => 'select',
666       '#default_value' => $this->options['element_wrapper_type'],
667       '#description' => $this->t('Choose the HTML element to wrap around this field and label, e.g. H1, H2, etc. This may not be used if the field and label are not rendered together, such as with a table.'),
668       '#states' => [
669         'visible' => [
670           ':input[name="options[element_wrapper_type_enable]"]' => ['checked' => TRUE],
671         ],
672       ],
673       '#fieldset' => 'style_settings',
674     ];
675
676     $form['element_wrapper_class_enable'] = [
677       '#type' => 'checkbox',
678       '#title' => $this->t('Create a CSS class'),
679       '#states' => [
680         'visible' => [
681           ':input[name="options[element_wrapper_type_enable]"]' => ['checked' => TRUE],
682         ],
683       ],
684       '#default_value' => !empty($this->options['element_wrapper_class']) || (string) $this->options['element_wrapper_class'] == '0',
685       '#fieldset' => 'style_settings',
686     ];
687     $form['element_wrapper_class'] = [
688       '#title' => $this->t('CSS class'),
689       '#description' => $this->t('You may use token substitutions from the rewriting section in this class.'),
690       '#type' => 'textfield',
691       '#default_value' => $this->options['element_wrapper_class'],
692       '#states' => [
693         'visible' => [
694           ':input[name="options[element_wrapper_class_enable]"]' => ['checked' => TRUE],
695           ':input[name="options[element_wrapper_type_enable]"]' => ['checked' => TRUE],
696         ],
697       ],
698       '#fieldset' => 'style_settings',
699     ];
700
701     $form['element_default_classes'] = [
702       '#type' => 'checkbox',
703       '#title' => $this->t('Add default classes'),
704       '#default_value' => $this->options['element_default_classes'],
705       '#description' => $this->t('Use default Views classes to identify the field, field label and field content.'),
706       '#fieldset' => 'style_settings',
707     ];
708
709     $form['alter'] = [
710       '#title' => $this->t('Rewrite results'),
711       '#type' => 'details',
712       '#weight' => 100,
713     ];
714
715     if ($this->allowAdvancedRender()) {
716       $form['alter']['#tree'] = TRUE;
717       $form['alter']['alter_text'] = [
718         '#type' => 'checkbox',
719         '#title' => $this->t('Override the output of this field with custom text'),
720         '#default_value' => $this->options['alter']['alter_text'],
721       ];
722
723       $form['alter']['text'] = [
724         '#title' => $this->t('Text'),
725         '#type' => 'textarea',
726         '#default_value' => $this->options['alter']['text'],
727         '#description' => $this->t('The text to display for this field. You may include HTML or <a href=":url">Twig</a>. You may enter data from this view as per the "Replacement patterns" below.', [':url' => CoreUrl::fromUri('http://twig.sensiolabs.org/documentation')->toString()]),
728         '#states' => [
729           'visible' => [
730             ':input[name="options[alter][alter_text]"]' => ['checked' => TRUE],
731           ],
732         ],
733       ];
734
735       $form['alter']['make_link'] = [
736         '#type' => 'checkbox',
737         '#title' => $this->t('Output this field as a custom link'),
738         '#default_value' => $this->options['alter']['make_link'],
739       ];
740       $form['alter']['path'] = [
741         '#title' => $this->t('Link path'),
742         '#type' => 'textfield',
743         '#default_value' => $this->options['alter']['path'],
744         '#description' => $this->t('The Drupal path or absolute URL for this link. You may enter data from this view as per the "Replacement patterns" below.'),
745         '#states' => [
746           'visible' => [
747             ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
748           ],
749         ],
750         '#maxlength' => 255,
751       ];
752       $form['alter']['absolute'] = [
753         '#type' => 'checkbox',
754         '#title' => $this->t('Use absolute path'),
755         '#default_value' => $this->options['alter']['absolute'],
756         '#states' => [
757           'visible' => [
758             ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
759           ],
760         ],
761       ];
762       $form['alter']['replace_spaces'] = [
763         '#type' => 'checkbox',
764         '#title' => $this->t('Replace spaces with dashes'),
765         '#default_value' => $this->options['alter']['replace_spaces'],
766         '#states' => [
767           'visible' => [
768             ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
769           ],
770         ],
771       ];
772       $form['alter']['external'] = [
773         '#type' => 'checkbox',
774         '#title' => $this->t('External server URL'),
775         '#default_value' => $this->options['alter']['external'],
776         '#description' => $this->t("Links to an external server using a full URL: e.g. 'http://www.example.com' or 'www.example.com'."),
777         '#states' => [
778           'visible' => [
779             ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
780           ],
781         ],
782       ];
783       $form['alter']['path_case'] = [
784         '#type' => 'select',
785         '#title' => $this->t('Transform the case'),
786         '#description' => $this->t('When printing URL paths, how to transform the case of the filter value.'),
787         '#states' => [
788           'visible' => [
789             ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
790           ],
791         ],
792         '#options' => [
793           'none' => $this->t('No transform'),
794           'upper' => $this->t('Upper case'),
795           'lower' => $this->t('Lower case'),
796           'ucfirst' => $this->t('Capitalize first letter'),
797           'ucwords' => $this->t('Capitalize each word'),
798         ],
799         '#default_value' => $this->options['alter']['path_case'],
800       ];
801       $form['alter']['link_class'] = [
802         '#title' => $this->t('Link class'),
803         '#type' => 'textfield',
804         '#default_value' => $this->options['alter']['link_class'],
805         '#description' => $this->t('The CSS class to apply to the link.'),
806         '#states' => [
807           'visible' => [
808             ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
809           ],
810         ],
811       ];
812       $form['alter']['alt'] = [
813         '#title' => $this->t('Title text'),
814         '#type' => 'textfield',
815         '#default_value' => $this->options['alter']['alt'],
816         '#description' => $this->t('Text to place as "title" text which most browsers display as a tooltip when hovering over the link.'),
817         '#states' => [
818           'visible' => [
819             ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
820           ],
821         ],
822       ];
823       $form['alter']['rel'] = [
824         '#title' => $this->t('Rel Text'),
825         '#type' => 'textfield',
826         '#default_value' => $this->options['alter']['rel'],
827         '#description' => $this->t('Include Rel attribute for use in lightbox2 or other javascript utility.'),
828         '#states' => [
829           'visible' => [
830             ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
831           ],
832         ],
833       ];
834       $form['alter']['prefix'] = [
835         '#title' => $this->t('Prefix text'),
836         '#type' => 'textfield',
837         '#default_value' => $this->options['alter']['prefix'],
838         '#description' => $this->t('Any text to display before this link. You may include HTML.'),
839         '#states' => [
840           'visible' => [
841             ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
842           ],
843         ],
844       ];
845       $form['alter']['suffix'] = [
846         '#title' => $this->t('Suffix text'),
847         '#type' => 'textfield',
848         '#default_value' => $this->options['alter']['suffix'],
849         '#description' => $this->t('Any text to display after this link. You may include HTML.'),
850         '#states' => [
851           'visible' => [
852             ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
853           ],
854         ],
855       ];
856       $form['alter']['target'] = [
857         '#title' => $this->t('Target'),
858         '#type' => 'textfield',
859         '#default_value' => $this->options['alter']['target'],
860         '#description' => $this->t("Target of the link, such as _blank, _parent or an iframe's name. This field is rarely used."),
861         '#states' => [
862           'visible' => [
863             ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
864           ],
865         ],
866       ];
867
868       // Get a list of the available fields and arguments for token replacement.
869
870       // Setup the tokens for fields.
871       $previous = $this->getPreviousFieldLabels();
872       $optgroup_arguments = (string) t('Arguments');
873       $optgroup_fields = (string) t('Fields');
874       foreach ($previous as $id => $label) {
875         $options[$optgroup_fields]["{{ $id }}"] = substr(strrchr($label, ":"), 2);
876       }
877       // Add the field to the list of options.
878       $options[$optgroup_fields]["{{ {$this->options['id']} }}"] = substr(strrchr($this->adminLabel(), ":"), 2);
879
880       foreach ($this->view->display_handler->getHandlers('argument') as $arg => $handler) {
881         $options[$optgroup_arguments]["{{ arguments.$arg }}"] = $this->t('@argument title', ['@argument' => $handler->adminLabel()]);
882         $options[$optgroup_arguments]["{{ raw_arguments.$arg }}"] = $this->t('@argument input', ['@argument' => $handler->adminLabel()]);
883       }
884
885       $this->documentSelfTokens($options[$optgroup_fields]);
886
887       // Default text.
888
889       $output = [];
890       $output[] = [
891         '#markup' => '<p>' . $this->t('You must add some additional fields to this display before using this field. These fields may be marked as <em>Exclude from display</em> if you prefer. Note that due to rendering order, you cannot use fields that come after this field; if you need a field not listed here, rearrange your fields.') . '</p>',
892       ];
893       // We have some options, so make a list.
894       if (!empty($options)) {
895         $output[] = [
896           '#markup' => '<p>' . $this->t("The following replacement tokens are available for this field. Note that due to rendering order, you cannot use fields that come after this field; if you need a field not listed here, rearrange your fields.") . '</p>',
897         ];
898         foreach (array_keys($options) as $type) {
899           if (!empty($options[$type])) {
900             $items = [];
901             foreach ($options[$type] as $key => $value) {
902               $items[] = $key . ' == ' . $value;
903             }
904             $item_list = [
905               '#theme' => 'item_list',
906               '#items' => $items,
907             ];
908             $output[] = $item_list;
909           }
910         }
911       }
912       // This construct uses 'hidden' and not markup because process doesn't
913       // run. It also has an extra div because the dependency wants to hide
914       // the parent in situations like this, so we need a second div to
915       // make this work.
916       $form['alter']['help'] = [
917         '#type' => 'details',
918         '#title' => $this->t('Replacement patterns'),
919         '#value' => $output,
920         '#states' => [
921           'visible' => [
922             [
923               ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
924             ],
925             [
926               ':input[name="options[alter][alter_text]"]' => ['checked' => TRUE],
927             ],
928             [
929               ':input[name="options[alter][more_link]"]' => ['checked' => TRUE],
930             ],
931           ],
932         ],
933       ];
934
935       $form['alter']['trim'] = [
936         '#type' => 'checkbox',
937         '#title' => $this->t('Trim this field to a maximum number of characters'),
938         '#default_value' => $this->options['alter']['trim'],
939       ];
940
941       $form['alter']['max_length'] = [
942         '#title' => $this->t('Maximum number of characters'),
943         '#type' => 'textfield',
944         '#default_value' => $this->options['alter']['max_length'],
945         '#states' => [
946           'visible' => [
947             ':input[name="options[alter][trim]"]' => ['checked' => TRUE],
948           ],
949         ],
950       ];
951
952       $form['alter']['word_boundary'] = [
953         '#type' => 'checkbox',
954         '#title' => $this->t('Trim only on a word boundary'),
955         '#description' => $this->t('If checked, this field be trimmed only on a word boundary. This is guaranteed to be the maximum characters stated or less. If there are no word boundaries this could trim a field to nothing.'),
956         '#default_value' => $this->options['alter']['word_boundary'],
957         '#states' => [
958           'visible' => [
959             ':input[name="options[alter][trim]"]' => ['checked' => TRUE],
960           ],
961         ],
962       ];
963
964       $form['alter']['ellipsis'] = [
965         '#type' => 'checkbox',
966         '#title' => $this->t('Add "…" at the end of trimmed text'),
967         '#default_value' => $this->options['alter']['ellipsis'],
968         '#states' => [
969           'visible' => [
970             ':input[name="options[alter][trim]"]' => ['checked' => TRUE],
971           ],
972         ],
973       ];
974
975       $form['alter']['more_link'] = [
976         '#type' => 'checkbox',
977         '#title' => $this->t('Add a read-more link if output is trimmed'),
978         '#default_value' => $this->options['alter']['more_link'],
979         '#states' => [
980           'visible' => [
981             ':input[name="options[alter][trim]"]' => ['checked' => TRUE],
982           ],
983         ],
984       ];
985
986       $form['alter']['more_link_text'] = [
987         '#type' => 'textfield',
988         '#title' => $this->t('More link label'),
989         '#default_value' => $this->options['alter']['more_link_text'],
990         '#description' => $this->t('You may use the "Replacement patterns" above.'),
991         '#states' => [
992           'visible' => [
993             ':input[name="options[alter][trim]"]' => ['checked' => TRUE],
994             ':input[name="options[alter][more_link]"]' => ['checked' => TRUE],
995           ],
996         ],
997       ];
998       $form['alter']['more_link_path'] = [
999         '#type' => 'textfield',
1000         '#title' => $this->t('More link path'),
1001         '#default_value' => $this->options['alter']['more_link_path'],
1002         '#description' => $this->t('This can be an internal Drupal path such as node/add or an external URL such as "https://www.drupal.org". You may use the "Replacement patterns" above.'),
1003         '#states' => [
1004           'visible' => [
1005             ':input[name="options[alter][trim]"]' => ['checked' => TRUE],
1006             ':input[name="options[alter][more_link]"]' => ['checked' => TRUE],
1007           ],
1008         ],
1009       ];
1010
1011       $form['alter']['html'] = [
1012         '#type' => 'checkbox',
1013         '#title' => $this->t('Field can contain HTML'),
1014         '#description' => $this->t('An HTML corrector will be run to ensure HTML tags are properly closed after trimming.'),
1015         '#default_value' => $this->options['alter']['html'],
1016         '#states' => [
1017           'visible' => [
1018             ':input[name="options[alter][trim]"]' => ['checked' => TRUE],
1019           ],
1020         ],
1021       ];
1022
1023       $form['alter']['strip_tags'] = [
1024         '#type' => 'checkbox',
1025         '#title' => $this->t('Strip HTML tags'),
1026         '#default_value' => $this->options['alter']['strip_tags'],
1027       ];
1028
1029       $form['alter']['preserve_tags'] = [
1030         '#type' => 'textfield',
1031         '#title' => $this->t('Preserve certain tags'),
1032         '#description' => $this->t('List the tags that need to be preserved during the stripping process. example &quot;&lt;p&gt; &lt;br&gt;&quot; which will preserve all p and br elements'),
1033         '#default_value' => $this->options['alter']['preserve_tags'],
1034         '#states' => [
1035           'visible' => [
1036             ':input[name="options[alter][strip_tags]"]' => ['checked' => TRUE],
1037           ],
1038         ],
1039       ];
1040
1041       $form['alter']['trim_whitespace'] = [
1042         '#type' => 'checkbox',
1043         '#title' => $this->t('Remove whitespace'),
1044         '#default_value' => $this->options['alter']['trim_whitespace'],
1045       ];
1046
1047       $form['alter']['nl2br'] = [
1048         '#type' => 'checkbox',
1049         '#title' => $this->t('Convert newlines to HTML &lt;br&gt; tags'),
1050         '#default_value' => $this->options['alter']['nl2br'],
1051       ];
1052     }
1053
1054     $form['empty_field_behavior'] = [
1055       '#type' => 'details',
1056       '#title' => $this->t('No results behavior'),
1057       '#weight' => 100,
1058     ];
1059
1060     $form['empty'] = [
1061       '#type' => 'textarea',
1062       '#title' => $this->t('No results text'),
1063       '#default_value' => $this->options['empty'],
1064       '#description' => $this->t('Provide text to display if this field contains an empty result. You may include HTML. You may enter data from this view as per the "Replacement patterns" in the "Rewrite Results" section above.'),
1065       '#fieldset' => 'empty_field_behavior',
1066     ];
1067
1068     $form['empty_zero'] = [
1069       '#type' => 'checkbox',
1070       '#title' => $this->t('Count the number 0 as empty'),
1071       '#default_value' => $this->options['empty_zero'],
1072       '#description' => $this->t('Enable to display the "no results text" if the field contains the number 0.'),
1073       '#fieldset' => 'empty_field_behavior',
1074     ];
1075
1076     $form['hide_empty'] = [
1077       '#type' => 'checkbox',
1078       '#title' => $this->t('Hide if empty'),
1079       '#default_value' => $this->options['hide_empty'],
1080       '#description' => $this->t('Enable to hide this field if it is empty. Note that the field label or rewritten output may still be displayed. To hide labels, check the style or row style settings for empty fields. To hide rewritten content, check the "Hide rewriting if empty" checkbox.'),
1081       '#fieldset' => 'empty_field_behavior',
1082     ];
1083
1084     $form['hide_alter_empty'] = [
1085       '#type' => 'checkbox',
1086       '#title' => $this->t('Hide rewriting if empty'),
1087       '#default_value' => $this->options['hide_alter_empty'],
1088       '#description' => $this->t('Do not display rewritten content if this field is empty.'),
1089       '#fieldset' => 'empty_field_behavior',
1090     ];
1091   }
1092
1093   /**
1094    * Returns all field labels of fields before this field.
1095    *
1096    * @return array
1097    *   An array of field labels keyed by their field IDs.
1098    */
1099   protected function getPreviousFieldLabels() {
1100     $all_fields = $this->view->display_handler->getFieldLabels();
1101     $field_options = array_slice($all_fields, 0, array_search($this->options['id'], array_keys($all_fields)));
1102     return $field_options;
1103   }
1104
1105   /**
1106    * Provide extra data to the administration form
1107    */
1108   public function adminSummary() {
1109     return $this->label();
1110   }
1111
1112   /**
1113    * {@inheritdoc}
1114    */
1115   public function preRender(&$values) {}
1116
1117   /**
1118    * {@inheritdoc}
1119    */
1120   public function render(ResultRow $values) {
1121     $value = $this->getValue($values);
1122     return $this->sanitizeValue($value);
1123   }
1124
1125   /**
1126    * {@inheritdoc}
1127    */
1128   public function postRender(ResultRow $row, $output) {
1129     // Make sure the last rendered value is available also when this is
1130     // retrieved from cache.
1131     $this->last_render = $output;
1132     return [];
1133   }
1134
1135   /**
1136    * {@inheritdoc}
1137    */
1138   public function advancedRender(ResultRow $values) {
1139     // Clean up values from previous render calls.
1140     if ($this->lastRenderIndex != $values->index) {
1141       $this->last_render_text = '';
1142     }
1143     if ($this->allowAdvancedRender() && $this instanceof MultiItemsFieldHandlerInterface) {
1144       $raw_items = $this->getItems($values);
1145       // If there are no items, set the original value to NULL.
1146       if (empty($raw_items)) {
1147         $this->original_value = NULL;
1148       }
1149     }
1150     else {
1151       $value = $this->render($values);
1152       if (is_array($value)) {
1153         $value = $this->getRenderer()->render($value);
1154       }
1155       $this->last_render = $value;
1156       $this->original_value = $value;
1157     }
1158
1159     if ($this->allowAdvancedRender()) {
1160       $tokens = NULL;
1161       if ($this instanceof MultiItemsFieldHandlerInterface) {
1162         $items = [];
1163         foreach ($raw_items as $count => $item) {
1164           $value = $this->render_item($count, $item);
1165           if (is_array($value)) {
1166             $value = (string) $this->getRenderer()->render($value);
1167           }
1168           $this->last_render = $value;
1169           $this->original_value = $this->last_render;
1170
1171           $alter = $item + $this->options['alter'];
1172           $alter['phase'] = static::RENDER_TEXT_PHASE_SINGLE_ITEM;
1173           $items[] = $this->renderText($alter);
1174         }
1175
1176         $value = $this->renderItems($items);
1177       }
1178       else {
1179         $alter = ['phase' => static::RENDER_TEXT_PHASE_COMPLETELY] + $this->options['alter'];
1180         $value = $this->renderText($alter);
1181       }
1182
1183       if (is_array($value)) {
1184         $value = $this->getRenderer()->render($value);
1185       }
1186       // This happens here so that renderAsLink can get the unaltered value of
1187       // this field as a token rather than the altered value.
1188       $this->last_render = $value;
1189     }
1190
1191     // String cast is necessary to test emptiness of MarkupInterface
1192     // objects.
1193     if (empty((string) $this->last_render)) {
1194       if ($this->isValueEmpty($this->last_render, $this->options['empty_zero'], FALSE)) {
1195         $alter = $this->options['alter'];
1196         $alter['alter_text'] = 1;
1197         $alter['text'] = $this->options['empty'];
1198         $alter['phase'] = static::RENDER_TEXT_PHASE_EMPTY;
1199         $this->last_render = $this->renderText($alter);
1200       }
1201     }
1202     // If we rendered something, update the last render index.
1203     if ((string) $this->last_render !== '') {
1204       $this->lastRenderIndex = $values->index;
1205     }
1206     return $this->last_render;
1207   }
1208
1209   /**
1210    * {@inheritdoc}
1211    */
1212   public function isValueEmpty($value, $empty_zero, $no_skip_empty = TRUE) {
1213     // Convert MarkupInterface to a string for checking.
1214     if ($value instanceof MarkupInterface) {
1215       $value = (string) $value;
1216     }
1217     if (!isset($value)) {
1218       $empty = TRUE;
1219     }
1220     else {
1221       $empty = ($empty_zero || ($value !== 0 && $value !== '0'));
1222     }
1223
1224     if ($no_skip_empty) {
1225       $empty = empty($value) && $empty;
1226     }
1227     return $empty;
1228   }
1229
1230   /**
1231    * {@inheritdoc}
1232    */
1233   public function renderText($alter) {
1234     // We need to preserve the safeness of the value regardless of the
1235     // alterations made by this method. Any alterations or replacements made
1236     // within this method need to ensure that at the minimum the result is
1237     // XSS admin filtered. See self::renderAltered() as an example that does.
1238     $value_is_safe = $this->last_render instanceof MarkupInterface;
1239     // Cast to a string so that empty checks and string functions work as
1240     // expected.
1241     $value = (string) $this->last_render;
1242
1243     if (!empty($alter['alter_text']) && $alter['text'] !== '') {
1244       $tokens = $this->getRenderTokens($alter);
1245       $value = $this->renderAltered($alter, $tokens);
1246       // $alter['text'] is entered through the views admin UI and will be safe
1247       // because the output of $this->renderAltered() is run through
1248       // Xss::filterAdmin().
1249       // @see \Drupal\views\Plugin\views\PluginBase::viewsTokenReplace()
1250       // @see \Drupal\Component\Utility\Xss::filterAdmin()
1251       $value_is_safe = TRUE;
1252     }
1253
1254     if (!empty($this->options['alter']['trim_whitespace'])) {
1255       $value = trim($value);
1256     }
1257
1258     // Check if there should be no further rewrite for empty values.
1259     $no_rewrite_for_empty = $this->options['hide_alter_empty'] && $this->isValueEmpty($this->original_value, $this->options['empty_zero']);
1260
1261     // Check whether the value is empty and return nothing, so the field isn't rendered.
1262     // First check whether the field should be hidden if the value(hide_alter_empty = TRUE) /the rewrite is empty (hide_alter_empty = FALSE).
1263     // For numeric values you can specify whether "0"/0 should be empty.
1264     if ((($this->options['hide_empty'] && empty($value))
1265         || ($alter['phase'] != static::RENDER_TEXT_PHASE_EMPTY && $no_rewrite_for_empty))
1266       && $this->isValueEmpty($value, $this->options['empty_zero'], FALSE)) {
1267       return '';
1268     }
1269     // Only in empty phase.
1270     if ($alter['phase'] == static::RENDER_TEXT_PHASE_EMPTY && $no_rewrite_for_empty) {
1271       // If we got here then $alter contains the value of "No results text"
1272       // and so there is nothing left to do.
1273       return ViewsRenderPipelineMarkup::create($value);
1274     }
1275
1276     if (!empty($alter['strip_tags'])) {
1277       $value = strip_tags($value, $alter['preserve_tags']);
1278     }
1279
1280     $more_link = '';
1281     if (!empty($alter['trim']) && !empty($alter['max_length'])) {
1282       $length = strlen($value);
1283       $value = $this->renderTrimText($alter, $value);
1284       if ($this->options['alter']['more_link'] && strlen($value) < $length) {
1285         $tokens = $this->getRenderTokens($alter);
1286         $more_link_text = $this->options['alter']['more_link_text'] ? $this->options['alter']['more_link_text'] : $this->t('more');
1287         $more_link_text = strtr(Xss::filterAdmin($more_link_text), $tokens);
1288         $more_link_path = $this->options['alter']['more_link_path'];
1289         $more_link_path = strip_tags(Html::decodeEntities($this->viewsTokenReplace($more_link_path, $tokens)));
1290
1291         // Make sure that paths which were run through URL generation work as
1292         // well.
1293         $base_path = base_path();
1294         // Checks whether the path starts with the base_path.
1295         if (strpos($more_link_path, $base_path) === 0) {
1296           $more_link_path = mb_substr($more_link_path, mb_strlen($base_path));
1297         }
1298
1299         // @todo Views should expect and store a leading /. See
1300         //   https://www.drupal.org/node/2423913.
1301         $options = [
1302           'attributes' => [
1303             'class' => [
1304               'views-more-link',
1305             ],
1306           ],
1307         ];
1308         if (UrlHelper::isExternal($more_link_path)) {
1309           $more_link_url = CoreUrl::fromUri($more_link_path, $options);
1310         }
1311         else {
1312           $more_link_url = CoreUrl::fromUserInput('/' . $more_link_path, $options);
1313         }
1314         $more_link = ' ' . $this->linkGenerator()->generate($more_link_text, $more_link_url);
1315       }
1316     }
1317
1318     if (!empty($alter['nl2br'])) {
1319       $value = nl2br($value);
1320     }
1321
1322     if ($value_is_safe) {
1323       $value = ViewsRenderPipelineMarkup::create($value);
1324     }
1325     $this->last_render_text = $value;
1326
1327     if (!empty($alter['make_link']) && (!empty($alter['path']) || !empty($alter['url']))) {
1328       if (!isset($tokens)) {
1329         $tokens = $this->getRenderTokens($alter);
1330       }
1331       $value = $this->renderAsLink($alter, $value, $tokens);
1332     }
1333
1334     // Preserve whether or not the string is safe. Since $more_link comes from
1335     // \Drupal::l(), it is safe to append. Check if the value is an instance of
1336     // \Drupal\Component\Render\MarkupInterface here because renderAsLink()
1337     // can return both safe and unsafe values.
1338     if ($value instanceof MarkupInterface) {
1339       return ViewsRenderPipelineMarkup::create($value . $more_link);
1340     }
1341     else {
1342       // If the string is not already marked safe, it is still OK to return it
1343       // because it will be sanitized by Twig.
1344       return $value . $more_link;
1345     }
1346   }
1347
1348   /**
1349    * Render this field as user-defined altered text.
1350    */
1351   protected function renderAltered($alter, $tokens) {
1352     return $this->viewsTokenReplace($alter['text'], $tokens);
1353   }
1354
1355   /**
1356    * Trims the field down to the specified length.
1357    *
1358    * @param array $alter
1359    *   The alter array of options to use.
1360    *     - max_length: Maximum length of the string, the rest gets truncated.
1361    *     - word_boundary: Trim only on a word boundary.
1362    *     - ellipsis: Show an ellipsis (…) at the end of the trimmed string.
1363    *     - html: Make sure that the html is correct.
1364    *
1365    * @param string $value
1366    *   The string which should be trimmed.
1367    *
1368    * @return string
1369    *   The rendered trimmed string.
1370    */
1371   protected function renderTrimText($alter, $value) {
1372     if (!empty($alter['strip_tags'])) {
1373       // NOTE: It's possible that some external fields might override the
1374       // element type.
1375       $this->definition['element type'] = 'span';
1376     }
1377     return static::trimText($alter, $value);
1378   }
1379
1380   /**
1381    * Render this field as a link, with the info from a fieldset set by
1382    * the user.
1383    */
1384   protected function renderAsLink($alter, $text, $tokens) {
1385     $options = [
1386       'absolute' => !empty($alter['absolute']) ? TRUE : FALSE,
1387       'alias' => FALSE,
1388       'entity' => NULL,
1389       'entity_type' => NULL,
1390       'fragment' => NULL,
1391       'language' => NULL,
1392       'query' => [],
1393     ];
1394
1395     $alter += [
1396       'path' => NULL,
1397     ];
1398
1399     $path = $alter['path'];
1400     // strip_tags() and viewsTokenReplace remove <front>, so check whether it's
1401     // different to front.
1402     if ($path != '<front>') {
1403       // Use strip_tags as there should never be HTML in the path.
1404       // However, we need to preserve special characters like " that were
1405       // removed by Html::escape().
1406       $path = Html::decodeEntities($this->viewsTokenReplace($alter['path'], $tokens));
1407
1408       // Tokens might contain <front>, so check for <front> again.
1409       if ($path != '<front>') {
1410         $path = strip_tags($path);
1411       }
1412
1413       // Tokens might have resolved URL's, as is the case for tokens provided by
1414       // Link fields, so all internal paths will be prefixed by base_path(). For
1415       // proper further handling reset this to internal:/.
1416       if (strpos($path, base_path()) === 0) {
1417         $path = 'internal:/' . substr($path, strlen(base_path()));
1418       }
1419
1420       // If we have no $path and no $alter['url'], we have nothing to work with,
1421       // so we just return the text.
1422       if (empty($path) && empty($alter['url'])) {
1423         return $text;
1424       }
1425
1426       // If no scheme is provided in the $path, assign the default 'http://'.
1427       // This allows a url of 'www.example.com' to be converted to
1428       // 'http://www.example.com'.
1429       // Only do this when flag for external has been set, $path doesn't contain
1430       // a scheme and $path doesn't have a leading /.
1431       if ($alter['external'] && !parse_url($path, PHP_URL_SCHEME) && strpos($path, '/') !== 0) {
1432         // There is no scheme, add the default 'http://' to the $path.
1433         $path = "http://" . $path;
1434       }
1435     }
1436
1437     if (empty($alter['url'])) {
1438       if (!parse_url($path, PHP_URL_SCHEME)) {
1439         // @todo Views should expect and store a leading /. See
1440         //   https://www.drupal.org/node/2423913.
1441         $alter['url'] = CoreUrl::fromUserInput('/' . ltrim($path, '/'));
1442       }
1443       else {
1444         $alter['url'] = CoreUrl::fromUri($path);
1445       }
1446     }
1447
1448     $options = $alter['url']->getOptions() + $options;
1449
1450     $path = $alter['url']->setOptions($options)->toUriString();
1451
1452     if (!empty($alter['path_case']) && $alter['path_case'] != 'none' && !$alter['url']->isRouted()) {
1453       $path = str_replace($alter['path'], $this->caseTransform($alter['path'], $this->options['alter']['path_case']), $path);
1454     }
1455
1456     if (!empty($alter['replace_spaces'])) {
1457       $path = str_replace(' ', '-', $path);
1458     }
1459
1460     // Parse the URL and move any query and fragment parameters out of the path.
1461     $url = UrlHelper::parse($path);
1462
1463     // Seriously malformed URLs may return FALSE or empty arrays.
1464     if (empty($url)) {
1465       return $text;
1466     }
1467
1468     // If the path is empty do not build a link around the given text and return
1469     // it as is.
1470     // http://www.example.com URLs will not have a $url['path'], so check host as well.
1471     if (empty($url['path']) && empty($url['host']) && empty($url['fragment']) && empty($url['url'])) {
1472       return $text;
1473     }
1474
1475     // If we get to here we have a path from the url parsing. So assign that to
1476     // $path now so we don't get query strings or fragments in the path.
1477     $path = $url['path'];
1478
1479     if (isset($url['query'])) {
1480       // Remove query parameters that were assigned a query string replacement
1481       // token for which there is no value available.
1482       foreach ($url['query'] as $param => $val) {
1483         if ($val == '%' . $param) {
1484           unset($url['query'][$param]);
1485         }
1486         // Replace any empty query params from URL parsing with NULL. So the
1487         // query will get built correctly with only the param key.
1488         // @see \Drupal\Component\Utility\UrlHelper::buildQuery().
1489         if ($val === '') {
1490           $url['query'][$param] = NULL;
1491         }
1492       }
1493
1494       $options['query'] = $url['query'];
1495     }
1496
1497     if (isset($url['fragment'])) {
1498       $path = strtr($path, ['#' . $url['fragment'] => '']);
1499       // If the path is empty we want to have a fragment for the current site.
1500       if ($path == '') {
1501         $options['external'] = TRUE;
1502       }
1503       $options['fragment'] = $url['fragment'];
1504     }
1505
1506     $alt = $this->viewsTokenReplace($alter['alt'], $tokens);
1507     // Set the title attribute of the link only if it improves accessibility
1508     if ($alt && $alt != $text) {
1509       $options['attributes']['title'] = Html::decodeEntities($alt);
1510     }
1511
1512     $class = $this->viewsTokenReplace($alter['link_class'], $tokens);
1513     if ($class) {
1514       $options['attributes']['class'] = [$class];
1515     }
1516
1517     if (!empty($alter['rel']) && $rel = $this->viewsTokenReplace($alter['rel'], $tokens)) {
1518       $options['attributes']['rel'] = $rel;
1519     }
1520
1521     $target = trim($this->viewsTokenReplace($alter['target'], $tokens));
1522     if (!empty($target)) {
1523       $options['attributes']['target'] = $target;
1524     }
1525
1526     // Allow the addition of arbitrary attributes to links. Additional attributes
1527     // currently can only be altered in preprocessors and not within the UI.
1528     if (isset($alter['link_attributes']) && is_array($alter['link_attributes'])) {
1529       foreach ($alter['link_attributes'] as $key => $attribute) {
1530         if (!isset($options['attributes'][$key])) {
1531           $options['attributes'][$key] = $this->viewsTokenReplace($attribute, $tokens);
1532         }
1533       }
1534     }
1535
1536     // If the query and fragment were programmatically assigned overwrite any
1537     // parsed values.
1538     if (isset($alter['query'])) {
1539       // Convert the query to a string, perform token replacement, and then
1540       // convert back to an array form for
1541       // \Drupal\Core\Utility\LinkGeneratorInterface::generate().
1542       $options['query'] = UrlHelper::buildQuery($alter['query']);
1543       $options['query'] = $this->viewsTokenReplace($options['query'], $tokens);
1544       $query = [];
1545       parse_str($options['query'], $query);
1546       $options['query'] = $query;
1547     }
1548     if (isset($alter['alias'])) {
1549       // Alias is a boolean field, so no token.
1550       $options['alias'] = $alter['alias'];
1551     }
1552     if (isset($alter['fragment'])) {
1553       $options['fragment'] = $this->viewsTokenReplace($alter['fragment'], $tokens);
1554     }
1555     if (isset($alter['language'])) {
1556       $options['language'] = $alter['language'];
1557     }
1558
1559     // If the url came from entity_uri(), pass along the required options.
1560     if (isset($alter['entity'])) {
1561       $options['entity'] = $alter['entity'];
1562     }
1563     if (isset($alter['entity_type'])) {
1564       $options['entity_type'] = $alter['entity_type'];
1565     }
1566
1567     // The path has been heavily processed above, so it should be used as-is.
1568     $final_url = CoreUrl::fromUri($path, $options);
1569
1570     // Build the link based on our altered Url object, adding on the optional
1571     // prefix and suffix
1572     $render = [
1573       '#type' => 'link',
1574       '#title' => $text,
1575       '#url' => $final_url,
1576     ];
1577
1578     if (!empty($alter['prefix'])) {
1579       $render['#prefix'] = $this->viewsTokenReplace($alter['prefix'], $tokens);
1580     }
1581     if (!empty($alter['suffix'])) {
1582       $render['#suffix'] = $this->viewsTokenReplace($alter['suffix'], $tokens);
1583     }
1584     return $this->getRenderer()->render($render);
1585
1586   }
1587
1588   /**
1589    * {@inheritdoc}
1590    */
1591   public function getRenderTokens($item) {
1592     $tokens = [];
1593     if (!empty($this->view->build_info['substitutions'])) {
1594       $tokens = $this->view->build_info['substitutions'];
1595     }
1596     $count = 0;
1597     foreach ($this->displayHandler->getHandlers('argument') as $arg => $handler) {
1598       $token = "{{ arguments.$arg }}";
1599       if (!isset($tokens[$token])) {
1600         $tokens[$token] = '';
1601       }
1602
1603       // Use strip tags as there should never be HTML in the path.
1604       // However, we need to preserve special characters like " that
1605       // were removed by Html::escape().
1606       $tokens["{{ raw_arguments.$arg }}"] = isset($this->view->args[$count]) ? strip_tags(Html::decodeEntities($this->view->args[$count])) : '';
1607       $count++;
1608     }
1609
1610     // Get flattened set of tokens for any array depth in query parameters.
1611     if ($request = $this->view->getRequest()) {
1612       $tokens += $this->getTokenValuesRecursive($request->query->all());
1613     }
1614
1615     // Now add replacements for our fields.
1616     foreach ($this->displayHandler->getHandlers('field') as $field => $handler) {
1617       /** @var static $handler */
1618       $placeholder = $handler->getFieldTokenPlaceholder();
1619
1620       if (isset($handler->last_render)) {
1621         $tokens[$placeholder] = $handler->last_render;
1622       }
1623       else {
1624         $tokens[$placeholder] = '';
1625       }
1626
1627       // We only use fields up to (and including) this one.
1628       if ($field == $this->options['id']) {
1629         break;
1630       }
1631     }
1632
1633     // Store the tokens for the row so we can reference them later if necessary.
1634     $this->view->style_plugin->render_tokens[$this->view->row_index] = $tokens;
1635     $this->last_tokens = $tokens;
1636     if (!empty($item)) {
1637       $this->addSelfTokens($tokens, $item);
1638     }
1639
1640     return $tokens;
1641   }
1642
1643   /**
1644    * Returns a token placeholder for the current field.
1645    *
1646    * @return string
1647    *   A token placeholder.
1648    */
1649   protected function getFieldTokenPlaceholder() {
1650     return '{{ ' . $this->options['id'] . ' }}';
1651   }
1652
1653   /**
1654    * Recursive function to add replacements for nested query string parameters.
1655    *
1656    * E.g. if you pass in the following array:
1657    *   array(
1658    *     'foo' => array(
1659    *       'a' => 'value',
1660    *       'b' => 'value',
1661    *     ),
1662    *     'bar' => array(
1663    *       'a' => 'value',
1664    *       'b' => array(
1665    *         'c' => value,
1666    *       ),
1667    *     ),
1668    *   );
1669    *
1670    * Would yield the following array of tokens:
1671    *   array(
1672    *     '%foo_a' => 'value'
1673    *     '%foo_b' => 'value'
1674    *     '%bar_a' => 'value'
1675    *     '%bar_b_c' => 'value'
1676    *   );
1677    *
1678    * @param $array
1679    *   An array of values.
1680    *
1681    * @param $parent_keys
1682    *   An array of parent keys. This will represent the array depth.
1683    *
1684    * @return
1685    *   An array of available tokens, with nested keys representative of the array structure.
1686    */
1687   protected function getTokenValuesRecursive(array $array, array $parent_keys = []) {
1688     $tokens = [];
1689
1690     foreach ($array as $param => $val) {
1691       if (is_array($val)) {
1692         // Copy parent_keys array, so we don't affect other elements of this
1693         // iteration.
1694         $child_parent_keys = $parent_keys;
1695         $child_parent_keys[] = $param;
1696         // Get the child tokens.
1697         $child_tokens = $this->getTokenValuesRecursive($val, $child_parent_keys);
1698         // Add them to the current tokens array.
1699         $tokens += $child_tokens;
1700       }
1701       else {
1702         // Create a token key based on array element structure.
1703         $token_string = !empty($parent_keys) ? implode('.', $parent_keys) . '.' . $param : $param;
1704         $tokens['{{ arguments.' . $token_string . ' }}'] = strip_tags(Html::decodeEntities($val));
1705       }
1706     }
1707
1708     return $tokens;
1709   }
1710
1711   /**
1712    * Add any special tokens this field might use for itself.
1713    *
1714    * This method is intended to be overridden by items that generate
1715    * fields as a list. For example, the field that displays all terms
1716    * on a node might have tokens for the tid and the term.
1717    *
1718    * By convention, tokens should follow the format of {{ token__subtoken }}
1719    * where token is the field ID and subtoken is the field. If the
1720    * field ID is terms, then the tokens might be {{ terms__tid }} and
1721    * {{ terms__name }}.
1722    */
1723   protected function addSelfTokens(&$tokens, $item) {}
1724
1725   /**
1726    * Document any special tokens this field might use for itself.
1727    *
1728    * @see addSelfTokens()
1729    */
1730   protected function documentSelfTokens(&$tokens) {}
1731
1732   /**
1733    * {@inheritdoc}
1734    */
1735   public function theme(ResultRow $values) {
1736     $renderer = $this->getRenderer();
1737     $build = [
1738       '#theme' => $this->themeFunctions(),
1739       '#view' => $this->view,
1740       '#field' => $this,
1741       '#row' => $values,
1742     ];
1743     $output = $renderer->render($build);
1744
1745     // Set the bubbleable rendering metadata on $view->element. This ensures the
1746     // bubbleable rendering metadata of individual rendered fields is not lost.
1747     // @see \Drupal\Core\Render\Renderer::updateStack()
1748     $this->view->element = $renderer->mergeBubbleableMetadata($this->view->element, $build);
1749
1750     return $output;
1751   }
1752
1753   public function themeFunctions() {
1754     $themes = [];
1755     $hook = 'views_view_field';
1756
1757     $display = $this->view->display_handler->display;
1758
1759     if (!empty($display)) {
1760       $themes[] = $hook . '__' . $this->view->storage->id() . '__' . $display['id'] . '__' . $this->options['id'];
1761       $themes[] = $hook . '__' . $this->view->storage->id() . '__' . $display['id'];
1762       $themes[] = $hook . '__' . $display['id'] . '__' . $this->options['id'];
1763       $themes[] = $hook . '__' . $display['id'];
1764       if ($display['id'] != $display['display_plugin']) {
1765         $themes[] = $hook . '__' . $this->view->storage->id() . '__' . $display['display_plugin'] . '__' . $this->options['id'];
1766         $themes[] = $hook . '__' . $this->view->storage->id() . '__' . $display['display_plugin'];
1767         $themes[] = $hook . '__' . $display['display_plugin'] . '__' . $this->options['id'];
1768         $themes[] = $hook . '__' . $display['display_plugin'];
1769       }
1770     }
1771     $themes[] = $hook . '__' . $this->view->storage->id() . '__' . $this->options['id'];
1772     $themes[] = $hook . '__' . $this->view->storage->id();
1773     $themes[] = $hook . '__' . $this->options['id'];
1774     $themes[] = $hook;
1775
1776     return $themes;
1777   }
1778
1779   public function adminLabel($short = FALSE) {
1780     return $this->getField(parent::adminLabel($short));
1781   }
1782
1783   /**
1784    * Trims the field down to the specified length.
1785    *
1786    * @param array $alter
1787    *   The alter array of options to use.
1788    *     - max_length: Maximum length of the string, the rest gets truncated.
1789    *     - word_boundary: Trim only on a word boundary.
1790    *     - ellipsis: Show an ellipsis (…) at the end of the trimmed string.
1791    *     - html: Make sure that the html is correct.
1792    *
1793    * @param string $value
1794    *   The string which should be trimmed.
1795    *
1796    * @return string
1797    *   The trimmed string.
1798    */
1799   public static function trimText($alter, $value) {
1800     if (mb_strlen($value) > $alter['max_length']) {
1801       $value = mb_substr($value, 0, $alter['max_length']);
1802       if (!empty($alter['word_boundary'])) {
1803         $regex = "(.*)\b.+";
1804         if (function_exists('mb_ereg')) {
1805           mb_regex_encoding('UTF-8');
1806           $found = mb_ereg($regex, $value, $matches);
1807         }
1808         else {
1809           $found = preg_match("/$regex/us", $value, $matches);
1810         }
1811         if ($found) {
1812           $value = $matches[1];
1813         }
1814       }
1815       // Remove scraps of HTML entities from the end of a strings
1816       $value = rtrim(preg_replace('/(?:<(?!.+>)|&(?!.+;)).*$/us', '', $value));
1817
1818       if (!empty($alter['ellipsis'])) {
1819         $value .= t('…');
1820       }
1821     }
1822     if (!empty($alter['html'])) {
1823       $value = Html::normalize($value);
1824     }
1825
1826     return $value;
1827   }
1828
1829   /**
1830    * Gets the link generator.
1831    *
1832    * @return \Drupal\Core\Utility\LinkGeneratorInterface
1833    */
1834   protected function linkGenerator() {
1835     if (!isset($this->linkGenerator)) {
1836       $this->linkGenerator = \Drupal::linkGenerator();
1837     }
1838     return $this->linkGenerator;
1839   }
1840
1841   /**
1842    * Returns the render API renderer.
1843    *
1844    * @return \Drupal\Core\Render\RendererInterface
1845    */
1846   protected function getRenderer() {
1847     if (!isset($this->renderer)) {
1848       $this->renderer = \Drupal::service('renderer');
1849     }
1850
1851     return $this->renderer;
1852   }
1853
1854 }
1855
1856 /**
1857  * @} End of "defgroup views_field_handlers".
1858  */