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