3 namespace Drupal\views\Plugin\views\field;
5 use Drupal\Component\Utility\Html;
6 use Drupal\Component\Render\MarkupInterface;
7 use Drupal\Component\Utility\UrlHelper;
8 use Drupal\Component\Utility\Xss;
9 use Drupal\Core\Form\FormStateInterface;
10 use Drupal\Core\Url as CoreUrl;
11 use Drupal\views\Plugin\views\HandlerBase;
12 use Drupal\views\Plugin\views\display\DisplayPluginBase;
13 use Drupal\views\Render\ViewsRenderPipelineMarkup;
14 use Drupal\views\ResultRow;
15 use Drupal\views\ViewExecutable;
18 * @defgroup views_field_handlers Views field handler plugins
20 * Handler plugins for Views fields.
22 * Field handlers handle both querying and display of fields in views.
24 * Field handler plugins extend
25 * \Drupal\views\Plugin\views\field\FieldPluginBase. They must be
26 * annotated with \Drupal\views\Annotation\ViewsField annotation, and they
27 * must be in namespace directory Plugin\views\field.
29 * The following items can go into a hook_views_data() implementation in a
30 * field section to affect how the field handler will behave:
31 * - additional fields: An array of fields that should be added to the query.
32 * The array is in one of these forms:
34 * // Simple form, for fields within the same table.
35 * array('identifier' => fieldname)
36 * // Form for fields in a different table.
37 * array('identifier' => array('table' => tablename, 'field' => fieldname))
39 * As many fields as are necessary may be in this array.
40 * - click sortable: If TRUE (default), this field may be click sorted.
42 * @ingroup views_plugins
47 * Base class for views fields.
49 * @ingroup views_field_handlers
51 abstract class FieldPluginBase extends HandlerBase implements FieldHandlerInterface {
54 * Indicator of the renderText() method for rendering a single item.
55 * (If no render_item() is present).
57 const RENDER_TEXT_PHASE_SINGLE_ITEM = 0;
60 * Indicator of the renderText() method for rendering the whole element.
61 * (if no render_item() method is available).
63 const RENDER_TEXT_PHASE_COMPLETELY = 1;
66 * Indicator of the renderText() method for rendering the empty text.
68 const RENDER_TEXT_PHASE_EMPTY = 2;
73 public $field_alias = 'unknown';
77 * The field value prior to any rewriting.
81 public $original_value = NULL;
84 * Stores additional fields which get added to the query.
86 * The generated aliases are stored in $aliases.
90 public $additional_fields = [];
95 * @var \Drupal\Core\Utility\LinkGeneratorInterface
97 protected $linkGenerator;
100 * Stores the render API renderer.
102 * @var \Drupal\Core\Render\RendererInterface
107 * Keeps track of the last render index.
111 protected $lastRenderIndex;
116 public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
117 parent::init($view, $display, $options);
119 $this->additional_fields = [];
120 if (!empty($this->definition['additional fields'])) {
121 $this->additional_fields = $this->definition['additional fields'];
124 if (!isset($this->options['exclude'])) {
125 $this->options['exclude'] = '';
130 * Determine if this field can allow advanced rendering.
132 * Fields can set this to FALSE if they do not wish to allow
133 * token based rewriting or link-making.
135 protected function allowAdvancedRender() {
140 * Called to add the field to a query.
142 public function query() {
143 $this->ensureMyTable();
145 $params = $this->options['group_type'] != 'group' ? ['function' => $this->options['group_type']] : [];
146 $this->field_alias = $this->query->addField($this->tableAlias, $this->realField, NULL, $params);
148 $this->addAdditionalFields();
152 * Add 'additional' fields to the query.
155 * An array of fields. The key is an identifier used to later find the
156 * field alias used. The value is either a string in which case it's
157 * assumed to be a field on this handler's table; or it's an array in the
159 * @code array('table' => $tablename, 'field' => $fieldname) @endcode
161 protected function addAdditionalFields($fields = NULL) {
162 if (!isset($fields)) {
164 if (empty($this->additional_fields)) {
167 $fields = $this->additional_fields;
171 if ($this->options['group_type'] != 'group') {
173 'function' => $this->options['group_type'],
177 if (!empty($fields) && is_array($fields)) {
178 foreach ($fields as $identifier => $info) {
179 if (is_array($info)) {
180 if (isset($info['table'])) {
181 $table_alias = $this->query->ensureTable($info['table'], $this->relationship);
184 $table_alias = $this->tableAlias;
187 if (empty($table_alias)) {
188 trigger_error(sprintf(
189 "Handler % tried to add additional_field %s but % could not be added!",
190 $this->definition['id'],
195 $this->aliases[$identifier] = 'broken';
200 if (!empty($info['params'])) {
201 $params = $info['params'];
204 $params += $group_params;
205 $this->aliases[$identifier] = $this->query->addField($table_alias, $info['field'], NULL, $params);
208 $this->aliases[$info] = $this->query->addField($this->tableAlias, $info, NULL, $group_params);
217 public function clickSort($order) {
218 if (isset($this->field_alias)) {
219 // Since fields should always have themselves already added, just
220 // add a sort on the field.
221 $params = $this->options['group_type'] != 'group' ? ['function' => $this->options['group_type']] : [];
222 $this->query->addOrderBy(NULL, NULL, $order, $this->field_alias, $params);
229 public function clickSortable() {
230 return isset($this->definition['click sortable']) ? $this->definition['click sortable'] : TRUE;
236 public function label() {
237 if (!isset($this->options['label'])) {
240 return $this->options['label'];
246 public function elementType($none_supported = FALSE, $default_empty = FALSE, $inline = FALSE) {
247 if ($none_supported) {
248 if ($this->options['element_type'] === '0') {
252 if ($this->options['element_type']) {
253 return $this->options['element_type'];
256 if ($default_empty) {
264 if (isset($this->definition['element type'])) {
265 return $this->definition['element type'];
274 public function elementLabelType($none_supported = FALSE, $default_empty = FALSE) {
275 if ($none_supported) {
276 if ($this->options['element_label_type'] === '0') {
280 if ($this->options['element_label_type']) {
281 return $this->options['element_label_type'];
284 if ($default_empty) {
294 public function elementWrapperType($none_supported = FALSE, $default_empty = FALSE) {
295 if ($none_supported) {
296 if ($this->options['element_wrapper_type'] === '0') {
300 if ($this->options['element_wrapper_type']) {
301 return $this->options['element_wrapper_type'];
304 if ($default_empty) {
314 public function getElements() {
315 static $elements = NULL;
316 if (!isset($elements)) {
317 // @todo Add possible html5 elements.
319 '' => $this->t('- Use default -'),
320 '0' => $this->t('- None -'),
322 $elements += \Drupal::config('views.settings')->get('field_rewrite_elements');
331 public function elementClasses($row_index = NULL) {
332 $classes = $this->tokenizeValue($this->options['element_class'], $row_index);
333 $classes = explode(' ', $classes);
334 foreach ($classes as &$class) {
335 $class = Html::cleanCssIdentifier($class);
337 return implode(' ', $classes);
343 public function tokenizeValue($value, $row_index = NULL) {
344 if (strpos($value, '{{') !== FALSE) {
346 'alter_text' => TRUE,
350 // Use isset() because empty() will trigger on 0 and 0 is
352 if (isset($row_index) && isset($this->view->style_plugin->render_tokens[$row_index])) {
353 $tokens = $this->view->style_plugin->render_tokens[$row_index];
356 // Get tokens from the last field.
357 $last_field = end($this->view->field);
358 if (isset($last_field->last_tokens)) {
359 $tokens = $last_field->last_tokens;
362 $tokens = $last_field->getRenderTokens($fake_item);
366 $value = strip_tags($this->renderAltered($fake_item, $tokens));
367 if (!empty($this->options['alter']['trim_whitespace'])) {
368 $value = trim($value);
378 public function elementLabelClasses($row_index = NULL) {
379 $classes = $this->tokenizeValue($this->options['element_label_class'], $row_index);
380 $classes = explode(' ', $classes);
381 foreach ($classes as &$class) {
382 $class = Html::cleanCssIdentifier($class);
384 return implode(' ', $classes);
390 public function elementWrapperClasses($row_index = NULL) {
391 $classes = $this->tokenizeValue($this->options['element_wrapper_class'], $row_index);
392 $classes = explode(' ', $classes);
393 foreach ($classes as &$class) {
394 $class = Html::cleanCssIdentifier($class);
396 return implode(' ', $classes);
402 public function getEntity(ResultRow $values) {
403 $relationship_id = $this->options['relationship'];
404 if ($relationship_id == 'none') {
405 return $values->_entity;
407 elseif (isset($values->_relationship_entities[$relationship_id])) {
408 return $values->_relationship_entities[$relationship_id];
415 public function getValue(ResultRow $values, $field = NULL) {
416 $alias = isset($field) ? $this->aliases[$field] : $this->field_alias;
417 if (isset($values->{$alias})) {
418 return $values->{$alias};
425 public function useStringGroupBy() {
429 protected function defineOptions() {
430 $options = parent::defineOptions();
432 $options['label'] = ['default' => ''];
433 // Some styles (for example table) should have labels enabled by default.
434 $style = $this->view->getStyle();
435 if (isset($style) && $style->defaultFieldLabels()) {
436 $options['label']['default'] = $this->definition['title'];
439 $options['exclude'] = ['default' => FALSE];
440 $options['alter'] = [
442 'alter_text' => ['default' => FALSE],
443 'text' => ['default' => ''],
444 'make_link' => ['default' => FALSE],
445 'path' => ['default' => ''],
446 'absolute' => ['default' => FALSE],
447 'external' => ['default' => FALSE],
448 'replace_spaces' => ['default' => FALSE],
449 'path_case' => ['default' => 'none'],
450 'trim_whitespace' => ['default' => FALSE],
451 'alt' => ['default' => ''],
452 'rel' => ['default' => ''],
453 'link_class' => ['default' => ''],
454 'prefix' => ['default' => ''],
455 'suffix' => ['default' => ''],
456 'target' => ['default' => ''],
457 'nl2br' => ['default' => FALSE],
458 'max_length' => ['default' => 0],
459 'word_boundary' => ['default' => TRUE],
460 'ellipsis' => ['default' => TRUE],
461 'more_link' => ['default' => FALSE],
462 'more_link_text' => ['default' => ''],
463 'more_link_path' => ['default' => ''],
464 'strip_tags' => ['default' => FALSE],
465 'trim' => ['default' => FALSE],
466 'preserve_tags' => ['default' => ''],
467 'html' => ['default' => FALSE],
470 $options['element_type'] = ['default' => ''];
471 $options['element_class'] = ['default' => ''];
473 $options['element_label_type'] = ['default' => ''];
474 $options['element_label_class'] = ['default' => ''];
475 $options['element_label_colon'] = ['default' => TRUE];
477 $options['element_wrapper_type'] = ['default' => ''];
478 $options['element_wrapper_class'] = ['default' => ''];
480 $options['element_default_classes'] = ['default' => TRUE];
482 $options['empty'] = ['default' => ''];
483 $options['hide_empty'] = ['default' => FALSE];
484 $options['empty_zero'] = ['default' => FALSE];
485 $options['hide_alter_empty'] = ['default' => TRUE];
491 * Performs some cleanup tasks on the options array before saving it.
493 public function submitOptionsForm(&$form, FormStateInterface $form_state) {
494 $options = &$form_state->getValue('options');
495 $types = ['element_type', 'element_label_type', 'element_wrapper_type'];
496 $classes = array_combine(['element_class', 'element_label_class', 'element_wrapper_class'], $types);
498 foreach ($types as $type) {
499 if (!$options[$type . '_enable']) {
500 $options[$type] = '';
504 foreach ($classes as $class => $type) {
505 if (!$options[$class . '_enable'] || !$options[$type . '_enable']) {
506 $options[$class] = '';
510 if (empty($options['custom_label'])) {
511 $options['label'] = '';
512 $options['element_label_colon'] = FALSE;
517 * Default options form that provides the label widget that all fields
520 public function buildOptionsForm(&$form, FormStateInterface $form_state) {
521 parent::buildOptionsForm($form, $form_state);
523 $label = $this->label();
524 $form['custom_label'] = [
525 '#type' => 'checkbox',
526 '#title' => $this->t('Create a label'),
527 '#default_value' => $label !== '',
531 '#type' => 'textfield',
532 '#title' => $this->t('Label'),
533 '#default_value' => $label,
536 ':input[name="options[custom_label]"]' => ['checked' => TRUE],
541 $form['element_label_colon'] = [
542 '#type' => 'checkbox',
543 '#title' => $this->t('Place a colon after the label'),
544 '#default_value' => $this->options['element_label_colon'],
547 ':input[name="options[custom_label]"]' => ['checked' => TRUE],
554 '#type' => 'checkbox',
555 '#title' => $this->t('Exclude from display'),
556 '#default_value' => $this->options['exclude'],
557 '#description' => $this->t('Enable to load this field as hidden. Often used to group fields, or to use as token in another field.'),
561 $form['style_settings'] = [
562 '#type' => 'details',
563 '#title' => $this->t('Style settings'),
567 $form['element_type_enable'] = [
568 '#type' => 'checkbox',
569 '#title' => $this->t('Customize field HTML'),
570 '#default_value' => !empty($this->options['element_type']) || (string) $this->options['element_type'] == '0' || !empty($this->options['element_class']) || (string) $this->options['element_class'] == '0',
571 '#fieldset' => 'style_settings',
573 $form['element_type'] = [
574 '#title' => $this->t('HTML element'),
575 '#options' => $this->getElements(),
577 '#default_value' => $this->options['element_type'],
578 '#description' => $this->t('Choose the HTML element to wrap around this field, e.g. H1, H2, etc.'),
581 ':input[name="options[element_type_enable]"]' => ['checked' => TRUE],
584 '#fieldset' => 'style_settings',
587 $form['element_class_enable'] = [
588 '#type' => 'checkbox',
589 '#title' => $this->t('Create a CSS class'),
592 ':input[name="options[element_type_enable]"]' => ['checked' => TRUE],
595 '#default_value' => !empty($this->options['element_class']) || (string) $this->options['element_class'] == '0',
596 '#fieldset' => 'style_settings',
598 $form['element_class'] = [
599 '#title' => $this->t('CSS class'),
600 '#description' => $this->t('You may use token substitutions from the rewriting section in this class.'),
601 '#type' => 'textfield',
602 '#default_value' => $this->options['element_class'],
605 ':input[name="options[element_type_enable]"]' => ['checked' => TRUE],
606 ':input[name="options[element_class_enable]"]' => ['checked' => TRUE],
609 '#fieldset' => 'style_settings',
612 $form['element_label_type_enable'] = [
613 '#type' => 'checkbox',
614 '#title' => $this->t('Customize label HTML'),
615 '#default_value' => !empty($this->options['element_label_type']) || (string) $this->options['element_label_type'] == '0' || !empty($this->options['element_label_class']) || (string) $this->options['element_label_class'] == '0',
616 '#fieldset' => 'style_settings',
618 $form['element_label_type'] = [
619 '#title' => $this->t('Label HTML element'),
620 '#options' => $this->getElements(FALSE),
622 '#default_value' => $this->options['element_label_type'],
623 '#description' => $this->t('Choose the HTML element to wrap around this label, e.g. H1, H2, etc.'),
626 ':input[name="options[element_label_type_enable]"]' => ['checked' => TRUE],
629 '#fieldset' => 'style_settings',
631 $form['element_label_class_enable'] = [
632 '#type' => 'checkbox',
633 '#title' => $this->t('Create a CSS class'),
636 ':input[name="options[element_label_type_enable]"]' => ['checked' => TRUE],
639 '#default_value' => !empty($this->options['element_label_class']) || (string) $this->options['element_label_class'] == '0',
640 '#fieldset' => 'style_settings',
642 $form['element_label_class'] = [
643 '#title' => $this->t('CSS class'),
644 '#description' => $this->t('You may use token substitutions from the rewriting section in this class.'),
645 '#type' => 'textfield',
646 '#default_value' => $this->options['element_label_class'],
649 ':input[name="options[element_label_type_enable]"]' => ['checked' => TRUE],
650 ':input[name="options[element_label_class_enable]"]' => ['checked' => TRUE],
653 '#fieldset' => 'style_settings',
656 $form['element_wrapper_type_enable'] = [
657 '#type' => 'checkbox',
658 '#title' => $this->t('Customize field and label wrapper HTML'),
659 '#default_value' => !empty($this->options['element_wrapper_type']) || (string) $this->options['element_wrapper_type'] == '0' || !empty($this->options['element_wrapper_class']) || (string) $this->options['element_wrapper_class'] == '0',
660 '#fieldset' => 'style_settings',
662 $form['element_wrapper_type'] = [
663 '#title' => $this->t('Wrapper HTML element'),
664 '#options' => $this->getElements(FALSE),
666 '#default_value' => $this->options['element_wrapper_type'],
667 '#description' => $this->t('Choose the HTML element to wrap around this field and label, e.g. H1, H2, etc. This may not be used if the field and label are not rendered together, such as with a table.'),
670 ':input[name="options[element_wrapper_type_enable]"]' => ['checked' => TRUE],
673 '#fieldset' => 'style_settings',
676 $form['element_wrapper_class_enable'] = [
677 '#type' => 'checkbox',
678 '#title' => $this->t('Create a CSS class'),
681 ':input[name="options[element_wrapper_type_enable]"]' => ['checked' => TRUE],
684 '#default_value' => !empty($this->options['element_wrapper_class']) || (string) $this->options['element_wrapper_class'] == '0',
685 '#fieldset' => 'style_settings',
687 $form['element_wrapper_class'] = [
688 '#title' => $this->t('CSS class'),
689 '#description' => $this->t('You may use token substitutions from the rewriting section in this class.'),
690 '#type' => 'textfield',
691 '#default_value' => $this->options['element_wrapper_class'],
694 ':input[name="options[element_wrapper_class_enable]"]' => ['checked' => TRUE],
695 ':input[name="options[element_wrapper_type_enable]"]' => ['checked' => TRUE],
698 '#fieldset' => 'style_settings',
701 $form['element_default_classes'] = [
702 '#type' => 'checkbox',
703 '#title' => $this->t('Add default classes'),
704 '#default_value' => $this->options['element_default_classes'],
705 '#description' => $this->t('Use default Views classes to identify the field, field label and field content.'),
706 '#fieldset' => 'style_settings',
710 '#title' => $this->t('Rewrite results'),
711 '#type' => 'details',
715 if ($this->allowAdvancedRender()) {
716 $form['alter']['#tree'] = TRUE;
717 $form['alter']['alter_text'] = [
718 '#type' => 'checkbox',
719 '#title' => $this->t('Override the output of this field with custom text'),
720 '#default_value' => $this->options['alter']['alter_text'],
723 $form['alter']['text'] = [
724 '#title' => $this->t('Text'),
725 '#type' => 'textarea',
726 '#default_value' => $this->options['alter']['text'],
727 '#description' => $this->t('The text to display for this field. You may include HTML or <a href=":url">Twig</a>. You may enter data from this view as per the "Replacement patterns" below.', [':url' => CoreUrl::fromUri('http://twig.sensiolabs.org/documentation')->toString()]),
730 ':input[name="options[alter][alter_text]"]' => ['checked' => TRUE],
735 $form['alter']['make_link'] = [
736 '#type' => 'checkbox',
737 '#title' => $this->t('Output this field as a custom link'),
738 '#default_value' => $this->options['alter']['make_link'],
740 $form['alter']['path'] = [
741 '#title' => $this->t('Link path'),
742 '#type' => 'textfield',
743 '#default_value' => $this->options['alter']['path'],
744 '#description' => $this->t('The Drupal path or absolute URL for this link. You may enter data from this view as per the "Replacement patterns" below.'),
747 ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
752 $form['alter']['absolute'] = [
753 '#type' => 'checkbox',
754 '#title' => $this->t('Use absolute path'),
755 '#default_value' => $this->options['alter']['absolute'],
758 ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
762 $form['alter']['replace_spaces'] = [
763 '#type' => 'checkbox',
764 '#title' => $this->t('Replace spaces with dashes'),
765 '#default_value' => $this->options['alter']['replace_spaces'],
768 ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
772 $form['alter']['external'] = [
773 '#type' => 'checkbox',
774 '#title' => $this->t('External server URL'),
775 '#default_value' => $this->options['alter']['external'],
776 '#description' => $this->t("Links to an external server using a full URL: e.g. 'http://www.example.com' or 'www.example.com'."),
779 ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
783 $form['alter']['path_case'] = [
785 '#title' => $this->t('Transform the case'),
786 '#description' => $this->t('When printing URL paths, how to transform the case of the filter value.'),
789 ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
793 'none' => $this->t('No transform'),
794 'upper' => $this->t('Upper case'),
795 'lower' => $this->t('Lower case'),
796 'ucfirst' => $this->t('Capitalize first letter'),
797 'ucwords' => $this->t('Capitalize each word'),
799 '#default_value' => $this->options['alter']['path_case'],
801 $form['alter']['link_class'] = [
802 '#title' => $this->t('Link class'),
803 '#type' => 'textfield',
804 '#default_value' => $this->options['alter']['link_class'],
805 '#description' => $this->t('The CSS class to apply to the link.'),
808 ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
812 $form['alter']['alt'] = [
813 '#title' => $this->t('Title text'),
814 '#type' => 'textfield',
815 '#default_value' => $this->options['alter']['alt'],
816 '#description' => $this->t('Text to place as "title" text which most browsers display as a tooltip when hovering over the link.'),
819 ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
823 $form['alter']['rel'] = [
824 '#title' => $this->t('Rel Text'),
825 '#type' => 'textfield',
826 '#default_value' => $this->options['alter']['rel'],
827 '#description' => $this->t('Include Rel attribute for use in lightbox2 or other javascript utility.'),
830 ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
834 $form['alter']['prefix'] = [
835 '#title' => $this->t('Prefix text'),
836 '#type' => 'textfield',
837 '#default_value' => $this->options['alter']['prefix'],
838 '#description' => $this->t('Any text to display before this link. You may include HTML.'),
841 ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
845 $form['alter']['suffix'] = [
846 '#title' => $this->t('Suffix text'),
847 '#type' => 'textfield',
848 '#default_value' => $this->options['alter']['suffix'],
849 '#description' => $this->t('Any text to display after this link. You may include HTML.'),
852 ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
856 $form['alter']['target'] = [
857 '#title' => $this->t('Target'),
858 '#type' => 'textfield',
859 '#default_value' => $this->options['alter']['target'],
860 '#description' => $this->t("Target of the link, such as _blank, _parent or an iframe's name. This field is rarely used."),
863 ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
868 // Get a list of the available fields and arguments for token replacement.
870 // Setup the tokens for fields.
871 $previous = $this->getPreviousFieldLabels();
872 $optgroup_arguments = (string) t('Arguments');
873 $optgroup_fields = (string) t('Fields');
874 foreach ($previous as $id => $label) {
875 $options[$optgroup_fields]["{{ $id }}"] = substr(strrchr($label, ":"), 2);
877 // Add the field to the list of options.
878 $options[$optgroup_fields]["{{ {$this->options['id']} }}"] = substr(strrchr($this->adminLabel(), ":"), 2);
880 foreach ($this->view->display_handler->getHandlers('argument') as $arg => $handler) {
881 $options[$optgroup_arguments]["{{ arguments.$arg }}"] = $this->t('@argument title', ['@argument' => $handler->adminLabel()]);
882 $options[$optgroup_arguments]["{{ raw_arguments.$arg }}"] = $this->t('@argument input', ['@argument' => $handler->adminLabel()]);
885 $this->documentSelfTokens($options[$optgroup_fields]);
891 '#markup' => '<p>' . $this->t('You must add some additional fields to this display before using this field. These fields may be marked as <em>Exclude from display</em> if you prefer. Note that due to rendering order, you cannot use fields that come after this field; if you need a field not listed here, rearrange your fields.') . '</p>',
893 // We have some options, so make a list.
894 if (!empty($options)) {
896 '#markup' => '<p>' . $this->t("The following replacement tokens are available for this field. Note that due to rendering order, you cannot use fields that come after this field; if you need a field not listed here, rearrange your fields.") . '</p>',
898 foreach (array_keys($options) as $type) {
899 if (!empty($options[$type])) {
901 foreach ($options[$type] as $key => $value) {
902 $items[] = $key . ' == ' . $value;
905 '#theme' => 'item_list',
908 $output[] = $item_list;
912 // This construct uses 'hidden' and not markup because process doesn't
913 // run. It also has an extra div because the dependency wants to hide
914 // the parent in situations like this, so we need a second div to
916 $form['alter']['help'] = [
917 '#type' => 'details',
918 '#title' => $this->t('Replacement patterns'),
923 ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
926 ':input[name="options[alter][alter_text]"]' => ['checked' => TRUE],
929 ':input[name="options[alter][more_link]"]' => ['checked' => TRUE],
935 $form['alter']['trim'] = [
936 '#type' => 'checkbox',
937 '#title' => $this->t('Trim this field to a maximum number of characters'),
938 '#default_value' => $this->options['alter']['trim'],
941 $form['alter']['max_length'] = [
942 '#title' => $this->t('Maximum number of characters'),
943 '#type' => 'textfield',
944 '#default_value' => $this->options['alter']['max_length'],
947 ':input[name="options[alter][trim]"]' => ['checked' => TRUE],
952 $form['alter']['word_boundary'] = [
953 '#type' => 'checkbox',
954 '#title' => $this->t('Trim only on a word boundary'),
955 '#description' => $this->t('If checked, this field be trimmed only on a word boundary. This is guaranteed to be the maximum characters stated or less. If there are no word boundaries this could trim a field to nothing.'),
956 '#default_value' => $this->options['alter']['word_boundary'],
959 ':input[name="options[alter][trim]"]' => ['checked' => TRUE],
964 $form['alter']['ellipsis'] = [
965 '#type' => 'checkbox',
966 '#title' => $this->t('Add "…" at the end of trimmed text'),
967 '#default_value' => $this->options['alter']['ellipsis'],
970 ':input[name="options[alter][trim]"]' => ['checked' => TRUE],
975 $form['alter']['more_link'] = [
976 '#type' => 'checkbox',
977 '#title' => $this->t('Add a read-more link if output is trimmed'),
978 '#default_value' => $this->options['alter']['more_link'],
981 ':input[name="options[alter][trim]"]' => ['checked' => TRUE],
986 $form['alter']['more_link_text'] = [
987 '#type' => 'textfield',
988 '#title' => $this->t('More link label'),
989 '#default_value' => $this->options['alter']['more_link_text'],
990 '#description' => $this->t('You may use the "Replacement patterns" above.'),
993 ':input[name="options[alter][trim]"]' => ['checked' => TRUE],
994 ':input[name="options[alter][more_link]"]' => ['checked' => TRUE],
998 $form['alter']['more_link_path'] = [
999 '#type' => 'textfield',
1000 '#title' => $this->t('More link path'),
1001 '#default_value' => $this->options['alter']['more_link_path'],
1002 '#description' => $this->t('This can be an internal Drupal path such as node/add or an external URL such as "https://www.drupal.org". You may use the "Replacement patterns" above.'),
1005 ':input[name="options[alter][trim]"]' => ['checked' => TRUE],
1006 ':input[name="options[alter][more_link]"]' => ['checked' => TRUE],
1011 $form['alter']['html'] = [
1012 '#type' => 'checkbox',
1013 '#title' => $this->t('Field can contain HTML'),
1014 '#description' => $this->t('An HTML corrector will be run to ensure HTML tags are properly closed after trimming.'),
1015 '#default_value' => $this->options['alter']['html'],
1018 ':input[name="options[alter][trim]"]' => ['checked' => TRUE],
1023 $form['alter']['strip_tags'] = [
1024 '#type' => 'checkbox',
1025 '#title' => $this->t('Strip HTML tags'),
1026 '#default_value' => $this->options['alter']['strip_tags'],
1029 $form['alter']['preserve_tags'] = [
1030 '#type' => 'textfield',
1031 '#title' => $this->t('Preserve certain tags'),
1032 '#description' => $this->t('List the tags that need to be preserved during the stripping process. example "<p> <br>" which will preserve all p and br elements'),
1033 '#default_value' => $this->options['alter']['preserve_tags'],
1036 ':input[name="options[alter][strip_tags]"]' => ['checked' => TRUE],
1041 $form['alter']['trim_whitespace'] = [
1042 '#type' => 'checkbox',
1043 '#title' => $this->t('Remove whitespace'),
1044 '#default_value' => $this->options['alter']['trim_whitespace'],
1047 $form['alter']['nl2br'] = [
1048 '#type' => 'checkbox',
1049 '#title' => $this->t('Convert newlines to HTML <br> tags'),
1050 '#default_value' => $this->options['alter']['nl2br'],
1054 $form['empty_field_behavior'] = [
1055 '#type' => 'details',
1056 '#title' => $this->t('No results behavior'),
1061 '#type' => 'textarea',
1062 '#title' => $this->t('No results text'),
1063 '#default_value' => $this->options['empty'],
1064 '#description' => $this->t('Provide text to display if this field contains an empty result. You may include HTML. You may enter data from this view as per the "Replacement patterns" in the "Rewrite Results" section above.'),
1065 '#fieldset' => 'empty_field_behavior',
1068 $form['empty_zero'] = [
1069 '#type' => 'checkbox',
1070 '#title' => $this->t('Count the number 0 as empty'),
1071 '#default_value' => $this->options['empty_zero'],
1072 '#description' => $this->t('Enable to display the "no results text" if the field contains the number 0.'),
1073 '#fieldset' => 'empty_field_behavior',
1076 $form['hide_empty'] = [
1077 '#type' => 'checkbox',
1078 '#title' => $this->t('Hide if empty'),
1079 '#default_value' => $this->options['hide_empty'],
1080 '#description' => $this->t('Enable to hide this field if it is empty. Note that the field label or rewritten output may still be displayed. To hide labels, check the style or row style settings for empty fields. To hide rewritten content, check the "Hide rewriting if empty" checkbox.'),
1081 '#fieldset' => 'empty_field_behavior',
1084 $form['hide_alter_empty'] = [
1085 '#type' => 'checkbox',
1086 '#title' => $this->t('Hide rewriting if empty'),
1087 '#default_value' => $this->options['hide_alter_empty'],
1088 '#description' => $this->t('Do not display rewritten content if this field is empty.'),
1089 '#fieldset' => 'empty_field_behavior',
1094 * Returns all field labels of fields before this field.
1097 * An array of field labels keyed by their field IDs.
1099 protected function getPreviousFieldLabels() {
1100 $all_fields = $this->view->display_handler->getFieldLabels();
1101 $field_options = array_slice($all_fields, 0, array_search($this->options['id'], array_keys($all_fields)));
1102 return $field_options;
1106 * Provide extra data to the administration form
1108 public function adminSummary() {
1109 return $this->label();
1115 public function preRender(&$values) {}
1120 public function render(ResultRow $values) {
1121 $value = $this->getValue($values);
1122 return $this->sanitizeValue($value);
1128 public function postRender(ResultRow $row, $output) {
1129 // Make sure the last rendered value is available also when this is
1130 // retrieved from cache.
1131 $this->last_render = $output;
1138 public function advancedRender(ResultRow $values) {
1139 // Clean up values from previous render calls.
1140 if ($this->lastRenderIndex != $values->index) {
1141 $this->last_render_text = '';
1143 if ($this->allowAdvancedRender() && $this instanceof MultiItemsFieldHandlerInterface) {
1144 $raw_items = $this->getItems($values);
1145 // If there are no items, set the original value to NULL.
1146 if (empty($raw_items)) {
1147 $this->original_value = NULL;
1151 $value = $this->render($values);
1152 if (is_array($value)) {
1153 $value = $this->getRenderer()->render($value);
1155 $this->last_render = $value;
1156 $this->original_value = $value;
1159 if ($this->allowAdvancedRender()) {
1161 if ($this instanceof MultiItemsFieldHandlerInterface) {
1163 foreach ($raw_items as $count => $item) {
1164 $value = $this->render_item($count, $item);
1165 if (is_array($value)) {
1166 $value = (string) $this->getRenderer()->render($value);
1168 $this->last_render = $value;
1169 $this->original_value = $this->last_render;
1171 $alter = $item + $this->options['alter'];
1172 $alter['phase'] = static::RENDER_TEXT_PHASE_SINGLE_ITEM;
1173 $items[] = $this->renderText($alter);
1176 $value = $this->renderItems($items);
1179 $alter = ['phase' => static::RENDER_TEXT_PHASE_COMPLETELY] + $this->options['alter'];
1180 $value = $this->renderText($alter);
1183 if (is_array($value)) {
1184 $value = $this->getRenderer()->render($value);
1186 // This happens here so that renderAsLink can get the unaltered value of
1187 // this field as a token rather than the altered value.
1188 $this->last_render = $value;
1191 // String cast is necessary to test emptiness of MarkupInterface
1193 if (empty((string) $this->last_render)) {
1194 if ($this->isValueEmpty($this->last_render, $this->options['empty_zero'], FALSE)) {
1195 $alter = $this->options['alter'];
1196 $alter['alter_text'] = 1;
1197 $alter['text'] = $this->options['empty'];
1198 $alter['phase'] = static::RENDER_TEXT_PHASE_EMPTY;
1199 $this->last_render = $this->renderText($alter);
1202 // If we rendered something, update the last render index.
1203 if ((string) $this->last_render !== '') {
1204 $this->lastRenderIndex = $values->index;
1206 return $this->last_render;
1212 public function isValueEmpty($value, $empty_zero, $no_skip_empty = TRUE) {
1213 // Convert MarkupInterface to a string for checking.
1214 if ($value instanceof MarkupInterface) {
1215 $value = (string) $value;
1217 if (!isset($value)) {
1221 $empty = ($empty_zero || ($value !== 0 && $value !== '0'));
1224 if ($no_skip_empty) {
1225 $empty = empty($value) && $empty;
1233 public function renderText($alter) {
1234 // We need to preserve the safeness of the value regardless of the
1235 // alterations made by this method. Any alterations or replacements made
1236 // within this method need to ensure that at the minimum the result is
1237 // XSS admin filtered. See self::renderAltered() as an example that does.
1238 $value_is_safe = $this->last_render instanceof MarkupInterface;
1239 // Cast to a string so that empty checks and string functions work as
1241 $value = (string) $this->last_render;
1243 if (!empty($alter['alter_text']) && $alter['text'] !== '') {
1244 $tokens = $this->getRenderTokens($alter);
1245 $value = $this->renderAltered($alter, $tokens);
1246 // $alter['text'] is entered through the views admin UI and will be safe
1247 // because the output of $this->renderAltered() is run through
1248 // Xss::filterAdmin().
1249 // @see \Drupal\views\Plugin\views\PluginBase::viewsTokenReplace()
1250 // @see \Drupal\Component\Utility\Xss::filterAdmin()
1251 $value_is_safe = TRUE;
1254 if (!empty($this->options['alter']['trim_whitespace'])) {
1255 $value = trim($value);
1258 // Check if there should be no further rewrite for empty values.
1259 $no_rewrite_for_empty = $this->options['hide_alter_empty'] && $this->isValueEmpty($this->original_value, $this->options['empty_zero']);
1261 // Check whether the value is empty and return nothing, so the field isn't rendered.
1262 // First check whether the field should be hidden if the value(hide_alter_empty = TRUE) /the rewrite is empty (hide_alter_empty = FALSE).
1263 // For numeric values you can specify whether "0"/0 should be empty.
1264 if ((($this->options['hide_empty'] && empty($value))
1265 || ($alter['phase'] != static::RENDER_TEXT_PHASE_EMPTY && $no_rewrite_for_empty))
1266 && $this->isValueEmpty($value, $this->options['empty_zero'], FALSE)) {
1269 // Only in empty phase.
1270 if ($alter['phase'] == static::RENDER_TEXT_PHASE_EMPTY && $no_rewrite_for_empty) {
1271 // If we got here then $alter contains the value of "No results text"
1272 // and so there is nothing left to do.
1273 return ViewsRenderPipelineMarkup::create($value);
1276 if (!empty($alter['strip_tags'])) {
1277 $value = strip_tags($value, $alter['preserve_tags']);
1281 if (!empty($alter['trim']) && !empty($alter['max_length'])) {
1282 $length = strlen($value);
1283 $value = $this->renderTrimText($alter, $value);
1284 if ($this->options['alter']['more_link'] && strlen($value) < $length) {
1285 $tokens = $this->getRenderTokens($alter);
1286 $more_link_text = $this->options['alter']['more_link_text'] ? $this->options['alter']['more_link_text'] : $this->t('more');
1287 $more_link_text = strtr(Xss::filterAdmin($more_link_text), $tokens);
1288 $more_link_path = $this->options['alter']['more_link_path'];
1289 $more_link_path = strip_tags(Html::decodeEntities($this->viewsTokenReplace($more_link_path, $tokens)));
1291 // Make sure that paths which were run through URL generation work as
1293 $base_path = base_path();
1294 // Checks whether the path starts with the base_path.
1295 if (strpos($more_link_path, $base_path) === 0) {
1296 $more_link_path = mb_substr($more_link_path, mb_strlen($base_path));
1299 // @todo Views should expect and store a leading /. See
1300 // https://www.drupal.org/node/2423913.
1308 if (UrlHelper::isExternal($more_link_path)) {
1309 $more_link_url = CoreUrl::fromUri($more_link_path, $options);
1312 $more_link_url = CoreUrl::fromUserInput('/' . $more_link_path, $options);
1314 $more_link = ' ' . $this->linkGenerator()->generate($more_link_text, $more_link_url);
1318 if (!empty($alter['nl2br'])) {
1319 $value = nl2br($value);
1322 if ($value_is_safe) {
1323 $value = ViewsRenderPipelineMarkup::create($value);
1325 $this->last_render_text = $value;
1327 if (!empty($alter['make_link']) && (!empty($alter['path']) || !empty($alter['url']))) {
1328 if (!isset($tokens)) {
1329 $tokens = $this->getRenderTokens($alter);
1331 $value = $this->renderAsLink($alter, $value, $tokens);
1334 // Preserve whether or not the string is safe. Since $more_link comes from
1335 // \Drupal::l(), it is safe to append. Check if the value is an instance of
1336 // \Drupal\Component\Render\MarkupInterface here because renderAsLink()
1337 // can return both safe and unsafe values.
1338 if ($value instanceof MarkupInterface) {
1339 return ViewsRenderPipelineMarkup::create($value . $more_link);
1342 // If the string is not already marked safe, it is still OK to return it
1343 // because it will be sanitized by Twig.
1344 return $value . $more_link;
1349 * Render this field as user-defined altered text.
1351 protected function renderAltered($alter, $tokens) {
1352 return $this->viewsTokenReplace($alter['text'], $tokens);
1356 * Trims the field down to the specified length.
1358 * @param array $alter
1359 * The alter array of options to use.
1360 * - max_length: Maximum length of the string, the rest gets truncated.
1361 * - word_boundary: Trim only on a word boundary.
1362 * - ellipsis: Show an ellipsis (…) at the end of the trimmed string.
1363 * - html: Make sure that the html is correct.
1365 * @param string $value
1366 * The string which should be trimmed.
1369 * The rendered trimmed string.
1371 protected function renderTrimText($alter, $value) {
1372 if (!empty($alter['strip_tags'])) {
1373 // NOTE: It's possible that some external fields might override the
1375 $this->definition['element type'] = 'span';
1377 return static::trimText($alter, $value);
1381 * Render this field as a link, with the info from a fieldset set by
1384 protected function renderAsLink($alter, $text, $tokens) {
1386 'absolute' => !empty($alter['absolute']) ? TRUE : FALSE,
1389 'entity_type' => NULL,
1399 $path = $alter['path'];
1400 // strip_tags() and viewsTokenReplace remove <front>, so check whether it's
1401 // different to front.
1402 if ($path != '<front>') {
1403 // Use strip_tags as there should never be HTML in the path.
1404 // However, we need to preserve special characters like " that were
1405 // removed by Html::escape().
1406 $path = Html::decodeEntities($this->viewsTokenReplace($alter['path'], $tokens));
1408 // Tokens might contain <front>, so check for <front> again.
1409 if ($path != '<front>') {
1410 $path = strip_tags($path);
1413 // Tokens might have resolved URL's, as is the case for tokens provided by
1414 // Link fields, so all internal paths will be prefixed by base_path(). For
1415 // proper further handling reset this to internal:/.
1416 if (strpos($path, base_path()) === 0) {
1417 $path = 'internal:/' . substr($path, strlen(base_path()));
1420 // If we have no $path and no $alter['url'], we have nothing to work with,
1421 // so we just return the text.
1422 if (empty($path) && empty($alter['url'])) {
1426 // If no scheme is provided in the $path, assign the default 'http://'.
1427 // This allows a url of 'www.example.com' to be converted to
1428 // 'http://www.example.com'.
1429 // Only do this when flag for external has been set, $path doesn't contain
1430 // a scheme and $path doesn't have a leading /.
1431 if ($alter['external'] && !parse_url($path, PHP_URL_SCHEME) && strpos($path, '/') !== 0) {
1432 // There is no scheme, add the default 'http://' to the $path.
1433 $path = "http://" . $path;
1437 if (empty($alter['url'])) {
1438 if (!parse_url($path, PHP_URL_SCHEME)) {
1439 // @todo Views should expect and store a leading /. See
1440 // https://www.drupal.org/node/2423913.
1441 $alter['url'] = CoreUrl::fromUserInput('/' . ltrim($path, '/'));
1444 $alter['url'] = CoreUrl::fromUri($path);
1448 $options = $alter['url']->getOptions() + $options;
1450 $path = $alter['url']->setOptions($options)->toUriString();
1452 if (!empty($alter['path_case']) && $alter['path_case'] != 'none' && !$alter['url']->isRouted()) {
1453 $path = str_replace($alter['path'], $this->caseTransform($alter['path'], $this->options['alter']['path_case']), $path);
1456 if (!empty($alter['replace_spaces'])) {
1457 $path = str_replace(' ', '-', $path);
1460 // Parse the URL and move any query and fragment parameters out of the path.
1461 $url = UrlHelper::parse($path);
1463 // Seriously malformed URLs may return FALSE or empty arrays.
1468 // If the path is empty do not build a link around the given text and return
1470 // http://www.example.com URLs will not have a $url['path'], so check host as well.
1471 if (empty($url['path']) && empty($url['host']) && empty($url['fragment']) && empty($url['url'])) {
1475 // If we get to here we have a path from the url parsing. So assign that to
1476 // $path now so we don't get query strings or fragments in the path.
1477 $path = $url['path'];
1479 if (isset($url['query'])) {
1480 // Remove query parameters that were assigned a query string replacement
1481 // token for which there is no value available.
1482 foreach ($url['query'] as $param => $val) {
1483 if ($val == '%' . $param) {
1484 unset($url['query'][$param]);
1486 // Replace any empty query params from URL parsing with NULL. So the
1487 // query will get built correctly with only the param key.
1488 // @see \Drupal\Component\Utility\UrlHelper::buildQuery().
1490 $url['query'][$param] = NULL;
1494 $options['query'] = $url['query'];
1497 if (isset($url['fragment'])) {
1498 $path = strtr($path, ['#' . $url['fragment'] => '']);
1499 // If the path is empty we want to have a fragment for the current site.
1501 $options['external'] = TRUE;
1503 $options['fragment'] = $url['fragment'];
1506 $alt = $this->viewsTokenReplace($alter['alt'], $tokens);
1507 // Set the title attribute of the link only if it improves accessibility
1508 if ($alt && $alt != $text) {
1509 $options['attributes']['title'] = Html::decodeEntities($alt);
1512 $class = $this->viewsTokenReplace($alter['link_class'], $tokens);
1514 $options['attributes']['class'] = [$class];
1517 if (!empty($alter['rel']) && $rel = $this->viewsTokenReplace($alter['rel'], $tokens)) {
1518 $options['attributes']['rel'] = $rel;
1521 $target = trim($this->viewsTokenReplace($alter['target'], $tokens));
1522 if (!empty($target)) {
1523 $options['attributes']['target'] = $target;
1526 // Allow the addition of arbitrary attributes to links. Additional attributes
1527 // currently can only be altered in preprocessors and not within the UI.
1528 if (isset($alter['link_attributes']) && is_array($alter['link_attributes'])) {
1529 foreach ($alter['link_attributes'] as $key => $attribute) {
1530 if (!isset($options['attributes'][$key])) {
1531 $options['attributes'][$key] = $this->viewsTokenReplace($attribute, $tokens);
1536 // If the query and fragment were programmatically assigned overwrite any
1538 if (isset($alter['query'])) {
1539 // Convert the query to a string, perform token replacement, and then
1540 // convert back to an array form for
1541 // \Drupal\Core\Utility\LinkGeneratorInterface::generate().
1542 $options['query'] = UrlHelper::buildQuery($alter['query']);
1543 $options['query'] = $this->viewsTokenReplace($options['query'], $tokens);
1545 parse_str($options['query'], $query);
1546 $options['query'] = $query;
1548 if (isset($alter['alias'])) {
1549 // Alias is a boolean field, so no token.
1550 $options['alias'] = $alter['alias'];
1552 if (isset($alter['fragment'])) {
1553 $options['fragment'] = $this->viewsTokenReplace($alter['fragment'], $tokens);
1555 if (isset($alter['language'])) {
1556 $options['language'] = $alter['language'];
1559 // If the url came from entity_uri(), pass along the required options.
1560 if (isset($alter['entity'])) {
1561 $options['entity'] = $alter['entity'];
1563 if (isset($alter['entity_type'])) {
1564 $options['entity_type'] = $alter['entity_type'];
1567 // The path has been heavily processed above, so it should be used as-is.
1568 $final_url = CoreUrl::fromUri($path, $options);
1570 // Build the link based on our altered Url object, adding on the optional
1571 // prefix and suffix
1575 '#url' => $final_url,
1578 if (!empty($alter['prefix'])) {
1579 $render['#prefix'] = $this->viewsTokenReplace($alter['prefix'], $tokens);
1581 if (!empty($alter['suffix'])) {
1582 $render['#suffix'] = $this->viewsTokenReplace($alter['suffix'], $tokens);
1584 return $this->getRenderer()->render($render);
1591 public function getRenderTokens($item) {
1593 if (!empty($this->view->build_info['substitutions'])) {
1594 $tokens = $this->view->build_info['substitutions'];
1597 foreach ($this->displayHandler->getHandlers('argument') as $arg => $handler) {
1598 $token = "{{ arguments.$arg }}";
1599 if (!isset($tokens[$token])) {
1600 $tokens[$token] = '';
1603 // Use strip tags as there should never be HTML in the path.
1604 // However, we need to preserve special characters like " that
1605 // were removed by Html::escape().
1606 $tokens["{{ raw_arguments.$arg }}"] = isset($this->view->args[$count]) ? strip_tags(Html::decodeEntities($this->view->args[$count])) : '';
1610 // Get flattened set of tokens for any array depth in query parameters.
1611 if ($request = $this->view->getRequest()) {
1612 $tokens += $this->getTokenValuesRecursive($request->query->all());
1615 // Now add replacements for our fields.
1616 foreach ($this->displayHandler->getHandlers('field') as $field => $handler) {
1617 /** @var static $handler */
1618 $placeholder = $handler->getFieldTokenPlaceholder();
1620 if (isset($handler->last_render)) {
1621 $tokens[$placeholder] = $handler->last_render;
1624 $tokens[$placeholder] = '';
1627 // We only use fields up to (and including) this one.
1628 if ($field == $this->options['id']) {
1633 // Store the tokens for the row so we can reference them later if necessary.
1634 $this->view->style_plugin->render_tokens[$this->view->row_index] = $tokens;
1635 $this->last_tokens = $tokens;
1636 if (!empty($item)) {
1637 $this->addSelfTokens($tokens, $item);
1644 * Returns a token placeholder for the current field.
1647 * A token placeholder.
1649 protected function getFieldTokenPlaceholder() {
1650 return '{{ ' . $this->options['id'] . ' }}';
1654 * Recursive function to add replacements for nested query string parameters.
1656 * E.g. if you pass in the following array:
1670 * Would yield the following array of tokens:
1672 * '%foo_a' => 'value'
1673 * '%foo_b' => 'value'
1674 * '%bar_a' => 'value'
1675 * '%bar_b_c' => 'value'
1679 * An array of values.
1681 * @param $parent_keys
1682 * An array of parent keys. This will represent the array depth.
1685 * An array of available tokens, with nested keys representative of the array structure.
1687 protected function getTokenValuesRecursive(array $array, array $parent_keys = []) {
1690 foreach ($array as $param => $val) {
1691 if (is_array($val)) {
1692 // Copy parent_keys array, so we don't affect other elements of this
1694 $child_parent_keys = $parent_keys;
1695 $child_parent_keys[] = $param;
1696 // Get the child tokens.
1697 $child_tokens = $this->getTokenValuesRecursive($val, $child_parent_keys);
1698 // Add them to the current tokens array.
1699 $tokens += $child_tokens;
1702 // Create a token key based on array element structure.
1703 $token_string = !empty($parent_keys) ? implode('.', $parent_keys) . '.' . $param : $param;
1704 $tokens['{{ arguments.' . $token_string . ' }}'] = strip_tags(Html::decodeEntities($val));
1712 * Add any special tokens this field might use for itself.
1714 * This method is intended to be overridden by items that generate
1715 * fields as a list. For example, the field that displays all terms
1716 * on a node might have tokens for the tid and the term.
1718 * By convention, tokens should follow the format of {{ token__subtoken }}
1719 * where token is the field ID and subtoken is the field. If the
1720 * field ID is terms, then the tokens might be {{ terms__tid }} and
1721 * {{ terms__name }}.
1723 protected function addSelfTokens(&$tokens, $item) {}
1726 * Document any special tokens this field might use for itself.
1728 * @see addSelfTokens()
1730 protected function documentSelfTokens(&$tokens) {}
1735 public function theme(ResultRow $values) {
1736 $renderer = $this->getRenderer();
1738 '#theme' => $this->themeFunctions(),
1739 '#view' => $this->view,
1743 $output = $renderer->render($build);
1745 // Set the bubbleable rendering metadata on $view->element. This ensures the
1746 // bubbleable rendering metadata of individual rendered fields is not lost.
1747 // @see \Drupal\Core\Render\Renderer::updateStack()
1748 $this->view->element = $renderer->mergeBubbleableMetadata($this->view->element, $build);
1753 public function themeFunctions() {
1755 $hook = 'views_view_field';
1757 $display = $this->view->display_handler->display;
1759 if (!empty($display)) {
1760 $themes[] = $hook . '__' . $this->view->storage->id() . '__' . $display['id'] . '__' . $this->options['id'];
1761 $themes[] = $hook . '__' . $this->view->storage->id() . '__' . $display['id'];
1762 $themes[] = $hook . '__' . $display['id'] . '__' . $this->options['id'];
1763 $themes[] = $hook . '__' . $display['id'];
1764 if ($display['id'] != $display['display_plugin']) {
1765 $themes[] = $hook . '__' . $this->view->storage->id() . '__' . $display['display_plugin'] . '__' . $this->options['id'];
1766 $themes[] = $hook . '__' . $this->view->storage->id() . '__' . $display['display_plugin'];
1767 $themes[] = $hook . '__' . $display['display_plugin'] . '__' . $this->options['id'];
1768 $themes[] = $hook . '__' . $display['display_plugin'];
1771 $themes[] = $hook . '__' . $this->view->storage->id() . '__' . $this->options['id'];
1772 $themes[] = $hook . '__' . $this->view->storage->id();
1773 $themes[] = $hook . '__' . $this->options['id'];
1779 public function adminLabel($short = FALSE) {
1780 return $this->getField(parent::adminLabel($short));
1784 * Trims the field down to the specified length.
1786 * @param array $alter
1787 * The alter array of options to use.
1788 * - max_length: Maximum length of the string, the rest gets truncated.
1789 * - word_boundary: Trim only on a word boundary.
1790 * - ellipsis: Show an ellipsis (…) at the end of the trimmed string.
1791 * - html: Make sure that the html is correct.
1793 * @param string $value
1794 * The string which should be trimmed.
1797 * The trimmed string.
1799 public static function trimText($alter, $value) {
1800 if (mb_strlen($value) > $alter['max_length']) {
1801 $value = mb_substr($value, 0, $alter['max_length']);
1802 if (!empty($alter['word_boundary'])) {
1803 $regex = "(.*)\b.+";
1804 if (function_exists('mb_ereg')) {
1805 mb_regex_encoding('UTF-8');
1806 $found = mb_ereg($regex, $value, $matches);
1809 $found = preg_match("/$regex/us", $value, $matches);
1812 $value = $matches[1];
1815 // Remove scraps of HTML entities from the end of a strings
1816 $value = rtrim(preg_replace('/(?:<(?!.+>)|&(?!.+;)).*$/us', '', $value));
1818 if (!empty($alter['ellipsis'])) {
1822 if (!empty($alter['html'])) {
1823 $value = Html::normalize($value);
1830 * Gets the link generator.
1832 * @return \Drupal\Core\Utility\LinkGeneratorInterface
1834 protected function linkGenerator() {
1835 if (!isset($this->linkGenerator)) {
1836 $this->linkGenerator = \Drupal::linkGenerator();
1838 return $this->linkGenerator;
1842 * Returns the render API renderer.
1844 * @return \Drupal\Core\Render\RendererInterface
1846 protected function getRenderer() {
1847 if (!isset($this->renderer)) {
1848 $this->renderer = \Drupal::service('renderer');
1851 return $this->renderer;
1857 * @} End of "defgroup views_field_handlers".