3 namespace Drupal\views\Plugin\views\field;
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;
19 * @defgroup views_field_handlers Views field handler plugins
21 * Handler plugins for Views fields.
23 * Field handlers handle both querying and display of fields in views.
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.
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:
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))
40 * As many fields as are necessary may be in this array.
41 * - click sortable: If TRUE (default), this field may be click sorted.
43 * @ingroup views_plugins
48 * Base class for views fields.
50 * @ingroup views_field_handlers
52 abstract class FieldPluginBase extends HandlerBase implements FieldHandlerInterface {
55 * Indicator of the renderText() method for rendering a single item.
56 * (If no render_item() is present).
58 const RENDER_TEXT_PHASE_SINGLE_ITEM = 0;
61 * Indicator of the renderText() method for rendering the whole element.
62 * (if no render_item() method is available).
64 const RENDER_TEXT_PHASE_COMPLETELY = 1;
67 * Indicator of the renderText() method for rendering the empty text.
69 const RENDER_TEXT_PHASE_EMPTY = 2;
74 public $field_alias = 'unknown';
78 * The field value prior to any rewriting.
82 public $original_value = NULL;
85 * Stores additional fields which get added to the query.
87 * The generated aliases are stored in $aliases.
91 public $additional_fields = [];
96 * @var \Drupal\Core\Utility\LinkGeneratorInterface
98 protected $linkGenerator;
101 * Stores the render API renderer.
103 * @var \Drupal\Core\Render\RendererInterface
108 * Keeps track of the last render index.
112 protected $lastRenderIndex;
117 public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
118 parent::init($view, $display, $options);
120 $this->additional_fields = [];
121 if (!empty($this->definition['additional fields'])) {
122 $this->additional_fields = $this->definition['additional fields'];
125 if (!isset($this->options['exclude'])) {
126 $this->options['exclude'] = '';
131 * Determine if this field can allow advanced rendering.
133 * Fields can set this to FALSE if they do not wish to allow
134 * token based rewriting or link-making.
136 protected function allowAdvancedRender() {
141 * Called to add the field to a query.
143 public function query() {
144 $this->ensureMyTable();
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);
149 $this->addAdditionalFields();
153 * Add 'additional' fields to the query.
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
160 * @code array('table' => $tablename, 'field' => $fieldname) @endcode
162 protected function addAdditionalFields($fields = NULL) {
163 if (!isset($fields)) {
165 if (empty($this->additional_fields)) {
168 $fields = $this->additional_fields;
172 if ($this->options['group_type'] != 'group') {
174 'function' => $this->options['group_type'],
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);
185 $table_alias = $this->tableAlias;
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';
195 if (!empty($info['params'])) {
196 $params = $info['params'];
199 $params += $group_params;
200 $this->aliases[$identifier] = $this->query->addField($table_alias, $info['field'], NULL, $params);
203 $this->aliases[$info] = $this->query->addField($this->tableAlias, $info, NULL, $group_params);
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);
224 public function clickSortable() {
225 return isset($this->definition['click sortable']) ? $this->definition['click sortable'] : TRUE;
231 public function label() {
232 if (!isset($this->options['label'])) {
235 return $this->options['label'];
241 public function elementType($none_supported = FALSE, $default_empty = FALSE, $inline = FALSE) {
242 if ($none_supported) {
243 if ($this->options['element_type'] === '0') {
247 if ($this->options['element_type']) {
248 return $this->options['element_type'];
251 if ($default_empty) {
259 if (isset($this->definition['element type'])) {
260 return $this->definition['element type'];
269 public function elementLabelType($none_supported = FALSE, $default_empty = FALSE) {
270 if ($none_supported) {
271 if ($this->options['element_label_type'] === '0') {
275 if ($this->options['element_label_type']) {
276 return $this->options['element_label_type'];
279 if ($default_empty) {
289 public function elementWrapperType($none_supported = FALSE, $default_empty = FALSE) {
290 if ($none_supported) {
291 if ($this->options['element_wrapper_type'] === '0') {
295 if ($this->options['element_wrapper_type']) {
296 return $this->options['element_wrapper_type'];
299 if ($default_empty) {
309 public function getElements() {
310 static $elements = NULL;
311 if (!isset($elements)) {
312 // @todo Add possible html5 elements.
314 '' => $this->t('- Use default -'),
315 '0' => $this->t('- None -')
317 $elements += \Drupal::config('views.settings')->get('field_rewrite_elements');
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);
332 return implode(' ', $classes);
338 public function tokenizeValue($value, $row_index = NULL) {
339 if (strpos($value, '{{') !== FALSE) {
341 'alter_text' => TRUE,
345 // Use isset() because empty() will trigger on 0 and 0 is
347 if (isset($row_index) && isset($this->view->style_plugin->render_tokens[$row_index])) {
348 $tokens = $this->view->style_plugin->render_tokens[$row_index];
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;
357 $tokens = $last_field->getRenderTokens($fake_item);
361 $value = strip_tags($this->renderAltered($fake_item, $tokens));
362 if (!empty($this->options['alter']['trim_whitespace'])) {
363 $value = trim($value);
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);
379 return implode(' ', $classes);
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);
391 return implode(' ', $classes);
397 public function getEntity(ResultRow $values) {
398 $relationship_id = $this->options['relationship'];
399 if ($relationship_id == 'none') {
400 return $values->_entity;
402 elseif (isset($values->_relationship_entities[$relationship_id])) {
403 return $values->_relationship_entities[$relationship_id];
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};
420 public function useStringGroupBy() {
424 protected function defineOptions() {
425 $options = parent::defineOptions();
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'];
434 $options['exclude'] = ['default' => FALSE];
435 $options['alter'] = [
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],
465 $options['element_type'] = ['default' => ''];
466 $options['element_class'] = ['default' => ''];
468 $options['element_label_type'] = ['default' => ''];
469 $options['element_label_class'] = ['default' => ''];
470 $options['element_label_colon'] = ['default' => TRUE];
472 $options['element_wrapper_type'] = ['default' => ''];
473 $options['element_wrapper_class'] = ['default' => ''];
475 $options['element_default_classes'] = ['default' => TRUE];
477 $options['empty'] = ['default' => ''];
478 $options['hide_empty'] = ['default' => FALSE];
479 $options['empty_zero'] = ['default' => FALSE];
480 $options['hide_alter_empty'] = ['default' => TRUE];
486 * Performs some cleanup tasks on the options array before saving it.
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);
493 foreach ($types as $type) {
494 if (!$options[$type . '_enable']) {
495 $options[$type] = '';
499 foreach ($classes as $class => $type) {
500 if (!$options[$class . '_enable'] || !$options[$type . '_enable']) {
501 $options[$class] = '';
505 if (empty($options['custom_label'])) {
506 $options['label'] = '';
507 $options['element_label_colon'] = FALSE;
512 * Default options form that provides the label widget that all fields
515 public function buildOptionsForm(&$form, FormStateInterface $form_state) {
516 parent::buildOptionsForm($form, $form_state);
518 $label = $this->label();
519 $form['custom_label'] = [
520 '#type' => 'checkbox',
521 '#title' => $this->t('Create a label'),
522 '#default_value' => $label !== '',
526 '#type' => 'textfield',
527 '#title' => $this->t('Label'),
528 '#default_value' => $label,
531 ':input[name="options[custom_label]"]' => ['checked' => TRUE],
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'],
542 ':input[name="options[custom_label]"]' => ['checked' => TRUE],
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.'),
556 $form['style_settings'] = [
557 '#type' => 'details',
558 '#title' => $this->t('Style settings'),
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',
568 $form['element_type'] = [
569 '#title' => $this->t('HTML element'),
570 '#options' => $this->getElements(),
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.'),
576 ':input[name="options[element_type_enable]"]' => ['checked' => TRUE],
579 '#fieldset' => 'style_settings',
582 $form['element_class_enable'] = [
583 '#type' => 'checkbox',
584 '#title' => $this->t('Create a CSS class'),
587 ':input[name="options[element_type_enable]"]' => ['checked' => TRUE],
590 '#default_value' => !empty($this->options['element_class']) || (string) $this->options['element_class'] == '0',
591 '#fieldset' => 'style_settings',
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'],
600 ':input[name="options[element_type_enable]"]' => ['checked' => TRUE],
601 ':input[name="options[element_class_enable]"]' => ['checked' => TRUE],
604 '#fieldset' => 'style_settings',
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',
613 $form['element_label_type'] = [
614 '#title' => $this->t('Label HTML element'),
615 '#options' => $this->getElements(FALSE),
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.'),
621 ':input[name="options[element_label_type_enable]"]' => ['checked' => TRUE],
624 '#fieldset' => 'style_settings',
626 $form['element_label_class_enable'] = [
627 '#type' => 'checkbox',
628 '#title' => $this->t('Create a CSS class'),
631 ':input[name="options[element_label_type_enable]"]' => ['checked' => TRUE],
634 '#default_value' => !empty($this->options['element_label_class']) || (string) $this->options['element_label_class'] == '0',
635 '#fieldset' => 'style_settings',
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'],
644 ':input[name="options[element_label_type_enable]"]' => ['checked' => TRUE],
645 ':input[name="options[element_label_class_enable]"]' => ['checked' => TRUE],
648 '#fieldset' => 'style_settings',
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',
657 $form['element_wrapper_type'] = [
658 '#title' => $this->t('Wrapper HTML element'),
659 '#options' => $this->getElements(FALSE),
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.'),
665 ':input[name="options[element_wrapper_type_enable]"]' => ['checked' => TRUE],
668 '#fieldset' => 'style_settings',
671 $form['element_wrapper_class_enable'] = [
672 '#type' => 'checkbox',
673 '#title' => $this->t('Create a CSS class'),
676 ':input[name="options[element_wrapper_type_enable]"]' => ['checked' => TRUE],
679 '#default_value' => !empty($this->options['element_wrapper_class']) || (string) $this->options['element_wrapper_class'] == '0',
680 '#fieldset' => 'style_settings',
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'],
689 ':input[name="options[element_wrapper_class_enable]"]' => ['checked' => TRUE],
690 ':input[name="options[element_wrapper_type_enable]"]' => ['checked' => TRUE],
693 '#fieldset' => 'style_settings',
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',
705 '#title' => $this->t('Rewrite results'),
706 '#type' => 'details',
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'],
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()]),
725 ':input[name="options[alter][alter_text]"]' => ['checked' => TRUE],
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'],
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.'),
742 ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
747 $form['alter']['absolute'] = [
748 '#type' => 'checkbox',
749 '#title' => $this->t('Use absolute path'),
750 '#default_value' => $this->options['alter']['absolute'],
753 ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
757 $form['alter']['replace_spaces'] = [
758 '#type' => 'checkbox',
759 '#title' => $this->t('Replace spaces with dashes'),
760 '#default_value' => $this->options['alter']['replace_spaces'],
763 ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
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'."),
774 ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
778 $form['alter']['path_case'] = [
780 '#title' => $this->t('Transform the case'),
781 '#description' => $this->t('When printing URL paths, how to transform the case of the filter value.'),
784 ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
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'),
794 '#default_value' => $this->options['alter']['path_case'],
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.'),
803 ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
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.'),
814 ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
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.'),
825 ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
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.'),
836 ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
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.'),
847 ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
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."),
858 ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
863 // Get a list of the available fields and arguments for token replacement.
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);
872 // Add the field to the list of options.
873 $options[$optgroup_fields]["{{ {$this->options['id']} }}"] = substr(strrchr($this->adminLabel(), ":"), 2);
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()]);
880 $this->documentSelfTokens($options[$optgroup_fields]);
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>',
888 // We have some options, so make a list.
889 if (!empty($options)) {
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>',
893 foreach (array_keys($options) as $type) {
894 if (!empty($options[$type])) {
896 foreach ($options[$type] as $key => $value) {
897 $items[] = $key . ' == ' . $value;
900 '#theme' => 'item_list',
903 $output[] = $item_list;
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
911 $form['alter']['help'] = [
912 '#type' => 'details',
913 '#title' => $this->t('Replacement patterns'),
918 ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
921 ':input[name="options[alter][alter_text]"]' => ['checked' => TRUE],
924 ':input[name="options[alter][more_link]"]' => ['checked' => TRUE],
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'],
936 $form['alter']['max_length'] = [
937 '#title' => $this->t('Maximum number of characters'),
938 '#type' => 'textfield',
939 '#default_value' => $this->options['alter']['max_length'],
942 ':input[name="options[alter][trim]"]' => ['checked' => TRUE],
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'],
954 ':input[name="options[alter][trim]"]' => ['checked' => TRUE],
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'],
965 ':input[name="options[alter][trim]"]' => ['checked' => TRUE],
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'],
976 ':input[name="options[alter][trim]"]' => ['checked' => TRUE],
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.'),
988 ':input[name="options[alter][trim]"]' => ['checked' => TRUE],
989 ':input[name="options[alter][more_link]"]' => ['checked' => TRUE],
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.'),
1000 ':input[name="options[alter][trim]"]' => ['checked' => TRUE],
1001 ':input[name="options[alter][more_link]"]' => ['checked' => TRUE],
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'],
1013 ':input[name="options[alter][trim]"]' => ['checked' => TRUE],
1018 $form['alter']['strip_tags'] = [
1019 '#type' => 'checkbox',
1020 '#title' => $this->t('Strip HTML tags'),
1021 '#default_value' => $this->options['alter']['strip_tags'],
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 "<p> <br>" which will preserve all p and br elements'),
1028 '#default_value' => $this->options['alter']['preserve_tags'],
1031 ':input[name="options[alter][strip_tags]"]' => ['checked' => TRUE],
1036 $form['alter']['trim_whitespace'] = [
1037 '#type' => 'checkbox',
1038 '#title' => $this->t('Remove whitespace'),
1039 '#default_value' => $this->options['alter']['trim_whitespace'],
1042 $form['alter']['nl2br'] = [
1043 '#type' => 'checkbox',
1044 '#title' => $this->t('Convert newlines to HTML <br> tags'),
1045 '#default_value' => $this->options['alter']['nl2br'],
1049 $form['empty_field_behavior'] = [
1050 '#type' => 'details',
1051 '#title' => $this->t('No results behavior'),
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',
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',
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',
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',
1089 * Returns all field labels of fields before this field.
1092 * An array of field labels keyed by their field IDs.
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;
1101 * Provide extra data to the administration form
1103 public function adminSummary() {
1104 return $this->label();
1110 public function preRender(&$values) {}
1115 public function render(ResultRow $values) {
1116 $value = $this->getValue($values);
1117 return $this->sanitizeValue($value);
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;
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 = '';
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;
1146 $value = $this->render($values);
1147 if (is_array($value)) {
1148 $value = $this->getRenderer()->render($value);
1150 $this->last_render = $value;
1151 $this->original_value = $value;
1154 if ($this->allowAdvancedRender()) {
1156 if ($this instanceof MultiItemsFieldHandlerInterface) {
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);
1163 $this->last_render = $value;
1164 $this->original_value = $this->last_render;
1166 $alter = $item + $this->options['alter'];
1167 $alter['phase'] = static::RENDER_TEXT_PHASE_SINGLE_ITEM;
1168 $items[] = $this->renderText($alter);
1171 $value = $this->renderItems($items);
1174 $alter = ['phase' => static::RENDER_TEXT_PHASE_COMPLETELY] + $this->options['alter'];
1175 $value = $this->renderText($alter);
1178 if (is_array($value)) {
1179 $value = $this->getRenderer()->render($value);
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;
1186 // String cast is necessary to test emptiness of MarkupInterface
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);
1197 // If we rendered something, update the last render index.
1198 if ((string) $this->last_render !== '') {
1199 $this->lastRenderIndex = $values->index;
1201 return $this->last_render;
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;
1212 if (!isset($value)) {
1216 $empty = ($empty_zero || ($value !== 0 && $value !== '0'));
1219 if ($no_skip_empty) {
1220 $empty = empty($value) && $empty;
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
1236 $value = (string) $this->last_render;
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;
1249 if (!empty($this->options['alter']['trim_whitespace'])) {
1250 $value = trim($value);
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']);
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)) {
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);
1271 if (!empty($alter['strip_tags'])) {
1272 $value = strip_tags($value, $alter['preserve_tags']);
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)));
1286 // Make sure that paths which were run through URL generation work as
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));
1294 // @todo Views should expect and store a leading /. See
1295 // https://www.drupal.org/node/2423913.
1303 if (UrlHelper::isExternal($more_link_path)) {
1304 $more_link_url = CoreUrl::fromUri($more_link_path, $options);
1307 $more_link_url = CoreUrl::fromUserInput('/' . $more_link_path, $options);
1309 $more_link = ' ' . $this->linkGenerator()->generate($more_link_text, $more_link_url);
1313 if (!empty($alter['nl2br'])) {
1314 $value = nl2br($value);
1317 if ($value_is_safe) {
1318 $value = ViewsRenderPipelineMarkup::create($value);
1320 $this->last_render_text = $value;
1322 if (!empty($alter['make_link']) && (!empty($alter['path']) || !empty($alter['url']))) {
1323 if (!isset($tokens)) {
1324 $tokens = $this->getRenderTokens($alter);
1326 $value = $this->renderAsLink($alter, $value, $tokens);
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);
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;
1344 * Render this field as user-defined altered text.
1346 protected function renderAltered($alter, $tokens) {
1347 return $this->viewsTokenReplace($alter['text'], $tokens);
1351 * Trims the field down to the specified length.
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.
1360 * @param string $value
1361 * The string which should be trimmed.
1364 * The rendered trimmed string.
1366 protected function renderTrimText($alter, $value) {
1367 if (!empty($alter['strip_tags'])) {
1368 // NOTE: It's possible that some external fields might override the
1370 $this->definition['element type'] = 'span';
1372 return static::trimText($alter, $value);
1376 * Render this field as a link, with the info from a fieldset set by
1379 protected function renderAsLink($alter, $text, $tokens) {
1381 'absolute' => !empty($alter['absolute']) ? TRUE : FALSE,
1384 'entity_type' => NULL,
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));
1403 // Tokens might contain <front>, so check for <front> again.
1404 if ($path != '<front>') {
1405 $path = strip_tags($path);
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()));
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'])) {
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;
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, '/'));
1439 $alter['url'] = CoreUrl::fromUri($path);
1443 $options = $alter['url']->getOptions() + $options;
1445 $path = $alter['url']->setOptions($options)->toUriString();
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);
1451 if (!empty($alter['replace_spaces'])) {
1452 $path = str_replace(' ', '-', $path);
1455 // Parse the URL and move any query and fragment parameters out of the path.
1456 $url = UrlHelper::parse($path);
1458 // Seriously malformed URLs may return FALSE or empty arrays.
1463 // If the path is empty do not build a link around the given text and return
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'])) {
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'];
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]);
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().
1485 $url['query'][$param] = NULL;
1489 $options['query'] = $url['query'];
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.
1496 $options['external'] = TRUE;
1498 $options['fragment'] = $url['fragment'];
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);
1507 $class = $this->viewsTokenReplace($alter['link_class'], $tokens);
1509 $options['attributes']['class'] = [$class];
1512 if (!empty($alter['rel']) && $rel = $this->viewsTokenReplace($alter['rel'], $tokens)) {
1513 $options['attributes']['rel'] = $rel;
1516 $target = trim($this->viewsTokenReplace($alter['target'], $tokens));
1517 if (!empty($target)) {
1518 $options['attributes']['target'] = $target;
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);
1531 // If the query and fragment were programmatically assigned overwrite any
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);
1540 parse_str($options['query'], $query);
1541 $options['query'] = $query;
1543 if (isset($alter['alias'])) {
1544 // Alias is a boolean field, so no token.
1545 $options['alias'] = $alter['alias'];
1547 if (isset($alter['fragment'])) {
1548 $options['fragment'] = $this->viewsTokenReplace($alter['fragment'], $tokens);
1550 if (isset($alter['language'])) {
1551 $options['language'] = $alter['language'];
1554 // If the url came from entity_uri(), pass along the required options.
1555 if (isset($alter['entity'])) {
1556 $options['entity'] = $alter['entity'];
1558 if (isset($alter['entity_type'])) {
1559 $options['entity_type'] = $alter['entity_type'];
1562 // The path has been heavily processed above, so it should be used as-is.
1563 $final_url = CoreUrl::fromUri($path, $options);
1565 // Build the link based on our altered Url object, adding on the optional
1566 // prefix and suffix
1570 '#url' => $final_url,
1573 if (!empty($alter['prefix'])) {
1574 $render['#prefix'] = $this->viewsTokenReplace($alter['prefix'], $tokens);
1576 if (!empty($alter['suffix'])) {
1577 $render['#suffix'] = $this->viewsTokenReplace($alter['suffix'], $tokens);
1579 return $this->getRenderer()->render($render);
1586 public function getRenderTokens($item) {
1588 if (!empty($this->view->build_info['substitutions'])) {
1589 $tokens = $this->view->build_info['substitutions'];
1592 foreach ($this->displayHandler->getHandlers('argument') as $arg => $handler) {
1593 $token = "{{ arguments.$arg }}";
1594 if (!isset($tokens[$token])) {
1595 $tokens[$token] = '';
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])) : '';
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());
1610 // Now add replacements for our fields.
1611 foreach ($this->displayHandler->getHandlers('field') as $field => $handler) {
1612 /** @var static $handler */
1613 $placeholder = $handler->getFieldTokenPlaceholder();
1615 if (isset($handler->last_render)) {
1616 $tokens[$placeholder] = $handler->last_render;
1619 $tokens[$placeholder] = '';
1622 // We only use fields up to (and including) this one.
1623 if ($field == $this->options['id']) {
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);
1639 * Returns a token placeholder for the current field.
1642 * A token placeholder.
1644 protected function getFieldTokenPlaceholder() {
1645 return '{{ ' . $this->options['id'] . ' }}';
1649 * Recursive function to add replacements for nested query string parameters.
1651 * E.g. if you pass in the following array:
1665 * Would yield the following array of tokens:
1667 * '%foo_a' => 'value'
1668 * '%foo_b' => 'value'
1669 * '%bar_a' => 'value'
1670 * '%bar_b_c' => 'value'
1674 * An array of values.
1676 * @param $parent_keys
1677 * An array of parent keys. This will represent the array depth.
1680 * An array of available tokens, with nested keys representative of the array structure.
1682 protected function getTokenValuesRecursive(array $array, array $parent_keys = []) {
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
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;
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));
1707 * Add any special tokens this field might use for itself.
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.
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 }}.
1718 protected function addSelfTokens(&$tokens, $item) {}
1721 * Document any special tokens this field might use for itself.
1723 * @see addSelfTokens()
1725 protected function documentSelfTokens(&$tokens) {}
1730 public function theme(ResultRow $values) {
1731 $renderer = $this->getRenderer();
1733 '#theme' => $this->themeFunctions(),
1734 '#view' => $this->view,
1738 $output = $renderer->render($build);
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);
1748 public function themeFunctions() {
1750 $hook = 'views_view_field';
1752 $display = $this->view->display_handler->display;
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'];
1766 $themes[] = $hook . '__' . $this->view->storage->id() . '__' . $this->options['id'];
1767 $themes[] = $hook . '__' . $this->view->storage->id();
1768 $themes[] = $hook . '__' . $this->options['id'];
1774 public function adminLabel($short = FALSE) {
1775 return $this->getField(parent::adminLabel($short));
1779 * Trims the field down to the specified length.
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.
1788 * @param string $value
1789 * The string which should be trimmed.
1792 * The trimmed string.
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);
1804 $found = preg_match("/$regex/us", $value, $matches);
1807 $value = $matches[1];
1810 // Remove scraps of HTML entities from the end of a strings
1811 $value = rtrim(preg_replace('/(?:<(?!.+>)|&(?!.+;)).*$/us', '', $value));
1813 if (!empty($alter['ellipsis'])) {
1817 if (!empty($alter['html'])) {
1818 $value = Html::normalize($value);
1825 * Gets the link generator.
1827 * @return \Drupal\Core\Utility\LinkGeneratorInterface
1829 protected function linkGenerator() {
1830 if (!isset($this->linkGenerator)) {
1831 $this->linkGenerator = \Drupal::linkGenerator();
1833 return $this->linkGenerator;
1837 * Returns the render API renderer.
1839 * @return \Drupal\Core\Render\RendererInterface
1841 protected function getRenderer() {
1842 if (!isset($this->renderer)) {
1843 $this->renderer = \Drupal::service('renderer');
1846 return $this->renderer;
1852 * @} End of "defgroup views_field_handlers".