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;
71 public $field_alias = 'unknown';
75 * The field value prior to any rewriting.
79 public $original_value = NULL;
82 * Stores additional fields which get added to the query.
84 * The generated aliases are stored in $aliases.
88 public $additional_fields = [];
93 * @var \Drupal\Core\Utility\LinkGeneratorInterface
95 protected $linkGenerator;
98 * Stores the render API renderer.
100 * @var \Drupal\Core\Render\RendererInterface
105 * Keeps track of the last render index.
109 protected $lastRenderIndex;
114 public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
115 parent::init($view, $display, $options);
117 $this->additional_fields = [];
118 if (!empty($this->definition['additional fields'])) {
119 $this->additional_fields = $this->definition['additional fields'];
122 if (!isset($this->options['exclude'])) {
123 $this->options['exclude'] = '';
128 * Determine if this field can allow advanced rendering.
130 * Fields can set this to FALSE if they do not wish to allow
131 * token based rewriting or link-making.
133 protected function allowAdvancedRender() {
138 * Called to add the field to a query.
140 public function query() {
141 $this->ensureMyTable();
143 $params = $this->options['group_type'] != 'group' ? ['function' => $this->options['group_type']] : [];
144 $this->field_alias = $this->query->addField($this->tableAlias, $this->realField, NULL, $params);
146 $this->addAdditionalFields();
150 * Add 'additional' fields to the query.
153 * An array of fields. The key is an identifier used to later find the
154 * field alias used. The value is either a string in which case it's
155 * assumed to be a field on this handler's table; or it's an array in the
157 * @code array('table' => $tablename, 'field' => $fieldname) @endcode
159 protected function addAdditionalFields($fields = NULL) {
160 if (!isset($fields)) {
162 if (empty($this->additional_fields)) {
165 $fields = $this->additional_fields;
169 if ($this->options['group_type'] != 'group') {
171 'function' => $this->options['group_type'],
175 if (!empty($fields) && is_array($fields)) {
176 foreach ($fields as $identifier => $info) {
177 if (is_array($info)) {
178 if (isset($info['table'])) {
179 $table_alias = $this->query->ensureTable($info['table'], $this->relationship);
182 $table_alias = $this->tableAlias;
185 if (empty($table_alias)) {
186 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']]));
187 $this->aliases[$identifier] = 'broken';
192 if (!empty($info['params'])) {
193 $params = $info['params'];
196 $params += $group_params;
197 $this->aliases[$identifier] = $this->query->addField($table_alias, $info['field'], NULL, $params);
200 $this->aliases[$info] = $this->query->addField($this->tableAlias, $info, NULL, $group_params);
209 public function clickSort($order) {
210 if (isset($this->field_alias)) {
211 // Since fields should always have themselves already added, just
212 // add a sort on the field.
213 $params = $this->options['group_type'] != 'group' ? ['function' => $this->options['group_type']] : [];
214 $this->query->addOrderBy(NULL, NULL, $order, $this->field_alias, $params);
221 public function clickSortable() {
222 return isset($this->definition['click sortable']) ? $this->definition['click sortable'] : TRUE;
228 public function label() {
229 if (!isset($this->options['label'])) {
232 return $this->options['label'];
238 public function elementType($none_supported = FALSE, $default_empty = FALSE, $inline = FALSE) {
239 if ($none_supported) {
240 if ($this->options['element_type'] === '0') {
244 if ($this->options['element_type']) {
245 return $this->options['element_type'];
248 if ($default_empty) {
256 if (isset($this->definition['element type'])) {
257 return $this->definition['element type'];
266 public function elementLabelType($none_supported = FALSE, $default_empty = FALSE) {
267 if ($none_supported) {
268 if ($this->options['element_label_type'] === '0') {
272 if ($this->options['element_label_type']) {
273 return $this->options['element_label_type'];
276 if ($default_empty) {
286 public function elementWrapperType($none_supported = FALSE, $default_empty = FALSE) {
287 if ($none_supported) {
288 if ($this->options['element_wrapper_type'] === '0') {
292 if ($this->options['element_wrapper_type']) {
293 return $this->options['element_wrapper_type'];
296 if ($default_empty) {
306 public function getElements() {
307 static $elements = NULL;
308 if (!isset($elements)) {
309 // @todo Add possible html5 elements.
311 '' => $this->t('- Use default -'),
312 '0' => $this->t('- None -')
314 $elements += \Drupal::config('views.settings')->get('field_rewrite_elements');
323 public function elementClasses($row_index = NULL) {
324 $classes = explode(' ', $this->options['element_class']);
325 foreach ($classes as &$class) {
326 $class = $this->tokenizeValue($class, $row_index);
327 $class = Html::cleanCssIdentifier($class);
329 return implode(' ', $classes);
335 public function tokenizeValue($value, $row_index = NULL) {
336 if (strpos($value, '{{') !== FALSE) {
338 'alter_text' => TRUE,
342 // Use isset() because empty() will trigger on 0 and 0 is
344 if (isset($row_index) && isset($this->view->style_plugin->render_tokens[$row_index])) {
345 $tokens = $this->view->style_plugin->render_tokens[$row_index];
348 // Get tokens from the last field.
349 $last_field = end($this->view->field);
350 if (isset($last_field->last_tokens)) {
351 $tokens = $last_field->last_tokens;
354 $tokens = $last_field->getRenderTokens($fake_item);
358 $value = strip_tags($this->renderAltered($fake_item, $tokens));
359 if (!empty($this->options['alter']['trim_whitespace'])) {
360 $value = trim($value);
370 public function elementLabelClasses($row_index = NULL) {
371 $classes = explode(' ', $this->options['element_label_class']);
372 foreach ($classes as &$class) {
373 $class = $this->tokenizeValue($class, $row_index);
374 $class = Html::cleanCssIdentifier($class);
376 return implode(' ', $classes);
382 public function elementWrapperClasses($row_index = NULL) {
383 $classes = explode(' ', $this->options['element_wrapper_class']);
384 foreach ($classes as &$class) {
385 $class = $this->tokenizeValue($class, $row_index);
386 $class = Html::cleanCssIdentifier($class);
388 return implode(' ', $classes);
394 public function getEntity(ResultRow $values) {
395 $relationship_id = $this->options['relationship'];
396 if ($relationship_id == 'none') {
397 return $values->_entity;
399 elseif (isset($values->_relationship_entities[$relationship_id])) {
400 return $values->_relationship_entities[$relationship_id];
407 public function getValue(ResultRow $values, $field = NULL) {
408 $alias = isset($field) ? $this->aliases[$field] : $this->field_alias;
409 if (isset($values->{$alias})) {
410 return $values->{$alias};
417 public function useStringGroupBy() {
421 protected function defineOptions() {
422 $options = parent::defineOptions();
424 $options['label'] = ['default' => ''];
425 // Some styles (for example table) should have labels enabled by default.
426 $style = $this->view->getStyle();
427 if (isset($style) && $style->defaultFieldLabels()) {
428 $options['label']['default'] = $this->definition['title'];
431 $options['exclude'] = ['default' => FALSE];
432 $options['alter'] = [
434 'alter_text' => ['default' => FALSE],
435 'text' => ['default' => ''],
436 'make_link' => ['default' => FALSE],
437 'path' => ['default' => ''],
438 'absolute' => ['default' => FALSE],
439 'external' => ['default' => FALSE],
440 'replace_spaces' => ['default' => FALSE],
441 'path_case' => ['default' => 'none'],
442 'trim_whitespace' => ['default' => FALSE],
443 'alt' => ['default' => ''],
444 'rel' => ['default' => ''],
445 'link_class' => ['default' => ''],
446 'prefix' => ['default' => ''],
447 'suffix' => ['default' => ''],
448 'target' => ['default' => ''],
449 'nl2br' => ['default' => FALSE],
450 'max_length' => ['default' => 0],
451 'word_boundary' => ['default' => TRUE],
452 'ellipsis' => ['default' => TRUE],
453 'more_link' => ['default' => FALSE],
454 'more_link_text' => ['default' => ''],
455 'more_link_path' => ['default' => ''],
456 'strip_tags' => ['default' => FALSE],
457 'trim' => ['default' => FALSE],
458 'preserve_tags' => ['default' => ''],
459 'html' => ['default' => FALSE],
462 $options['element_type'] = ['default' => ''];
463 $options['element_class'] = ['default' => ''];
465 $options['element_label_type'] = ['default' => ''];
466 $options['element_label_class'] = ['default' => ''];
467 $options['element_label_colon'] = ['default' => TRUE];
469 $options['element_wrapper_type'] = ['default' => ''];
470 $options['element_wrapper_class'] = ['default' => ''];
472 $options['element_default_classes'] = ['default' => TRUE];
474 $options['empty'] = ['default' => ''];
475 $options['hide_empty'] = ['default' => FALSE];
476 $options['empty_zero'] = ['default' => FALSE];
477 $options['hide_alter_empty'] = ['default' => TRUE];
483 * Performs some cleanup tasks on the options array before saving it.
485 public function submitOptionsForm(&$form, FormStateInterface $form_state) {
486 $options = &$form_state->getValue('options');
487 $types = ['element_type', 'element_label_type', 'element_wrapper_type'];
488 $classes = array_combine(['element_class', 'element_label_class', 'element_wrapper_class'], $types);
490 foreach ($types as $type) {
491 if (!$options[$type . '_enable']) {
492 $options[$type] = '';
496 foreach ($classes as $class => $type) {
497 if (!$options[$class . '_enable'] || !$options[$type . '_enable']) {
498 $options[$class] = '';
502 if (empty($options['custom_label'])) {
503 $options['label'] = '';
504 $options['element_label_colon'] = FALSE;
509 * Default options form that provides the label widget that all fields
512 public function buildOptionsForm(&$form, FormStateInterface $form_state) {
513 parent::buildOptionsForm($form, $form_state);
515 $label = $this->label();
516 $form['custom_label'] = [
517 '#type' => 'checkbox',
518 '#title' => $this->t('Create a label'),
519 '#default_value' => $label !== '',
523 '#type' => 'textfield',
524 '#title' => $this->t('Label'),
525 '#default_value' => $label,
528 ':input[name="options[custom_label]"]' => ['checked' => TRUE],
533 $form['element_label_colon'] = [
534 '#type' => 'checkbox',
535 '#title' => $this->t('Place a colon after the label'),
536 '#default_value' => $this->options['element_label_colon'],
539 ':input[name="options[custom_label]"]' => ['checked' => TRUE],
546 '#type' => 'checkbox',
547 '#title' => $this->t('Exclude from display'),
548 '#default_value' => $this->options['exclude'],
549 '#description' => $this->t('Enable to load this field as hidden. Often used to group fields, or to use as token in another field.'),
553 $form['style_settings'] = [
554 '#type' => 'details',
555 '#title' => $this->t('Style settings'),
559 $form['element_type_enable'] = [
560 '#type' => 'checkbox',
561 '#title' => $this->t('Customize field HTML'),
562 '#default_value' => !empty($this->options['element_type']) || (string) $this->options['element_type'] == '0' || !empty($this->options['element_class']) || (string) $this->options['element_class'] == '0',
563 '#fieldset' => 'style_settings',
565 $form['element_type'] = [
566 '#title' => $this->t('HTML element'),
567 '#options' => $this->getElements(),
569 '#default_value' => $this->options['element_type'],
570 '#description' => $this->t('Choose the HTML element to wrap around this field, e.g. H1, H2, etc.'),
573 ':input[name="options[element_type_enable]"]' => ['checked' => TRUE],
576 '#fieldset' => 'style_settings',
579 $form['element_class_enable'] = [
580 '#type' => 'checkbox',
581 '#title' => $this->t('Create a CSS class'),
584 ':input[name="options[element_type_enable]"]' => ['checked' => TRUE],
587 '#default_value' => !empty($this->options['element_class']) || (string) $this->options['element_class'] == '0',
588 '#fieldset' => 'style_settings',
590 $form['element_class'] = [
591 '#title' => $this->t('CSS class'),
592 '#description' => $this->t('You may use token substitutions from the rewriting section in this class.'),
593 '#type' => 'textfield',
594 '#default_value' => $this->options['element_class'],
597 ':input[name="options[element_type_enable]"]' => ['checked' => TRUE],
598 ':input[name="options[element_class_enable]"]' => ['checked' => TRUE],
601 '#fieldset' => 'style_settings',
604 $form['element_label_type_enable'] = [
605 '#type' => 'checkbox',
606 '#title' => $this->t('Customize label HTML'),
607 '#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',
608 '#fieldset' => 'style_settings',
610 $form['element_label_type'] = [
611 '#title' => $this->t('Label HTML element'),
612 '#options' => $this->getElements(FALSE),
614 '#default_value' => $this->options['element_label_type'],
615 '#description' => $this->t('Choose the HTML element to wrap around this label, e.g. H1, H2, etc.'),
618 ':input[name="options[element_label_type_enable]"]' => ['checked' => TRUE],
621 '#fieldset' => 'style_settings',
623 $form['element_label_class_enable'] = [
624 '#type' => 'checkbox',
625 '#title' => $this->t('Create a CSS class'),
628 ':input[name="options[element_label_type_enable]"]' => ['checked' => TRUE],
631 '#default_value' => !empty($this->options['element_label_class']) || (string) $this->options['element_label_class'] == '0',
632 '#fieldset' => 'style_settings',
634 $form['element_label_class'] = [
635 '#title' => $this->t('CSS class'),
636 '#description' => $this->t('You may use token substitutions from the rewriting section in this class.'),
637 '#type' => 'textfield',
638 '#default_value' => $this->options['element_label_class'],
641 ':input[name="options[element_label_type_enable]"]' => ['checked' => TRUE],
642 ':input[name="options[element_label_class_enable]"]' => ['checked' => TRUE],
645 '#fieldset' => 'style_settings',
648 $form['element_wrapper_type_enable'] = [
649 '#type' => 'checkbox',
650 '#title' => $this->t('Customize field and label wrapper HTML'),
651 '#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',
652 '#fieldset' => 'style_settings',
654 $form['element_wrapper_type'] = [
655 '#title' => $this->t('Wrapper HTML element'),
656 '#options' => $this->getElements(FALSE),
658 '#default_value' => $this->options['element_wrapper_type'],
659 '#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.'),
662 ':input[name="options[element_wrapper_type_enable]"]' => ['checked' => TRUE],
665 '#fieldset' => 'style_settings',
668 $form['element_wrapper_class_enable'] = [
669 '#type' => 'checkbox',
670 '#title' => $this->t('Create a CSS class'),
673 ':input[name="options[element_wrapper_type_enable]"]' => ['checked' => TRUE],
676 '#default_value' => !empty($this->options['element_wrapper_class']) || (string) $this->options['element_wrapper_class'] == '0',
677 '#fieldset' => 'style_settings',
679 $form['element_wrapper_class'] = [
680 '#title' => $this->t('CSS class'),
681 '#description' => $this->t('You may use token substitutions from the rewriting section in this class.'),
682 '#type' => 'textfield',
683 '#default_value' => $this->options['element_wrapper_class'],
686 ':input[name="options[element_wrapper_class_enable]"]' => ['checked' => TRUE],
687 ':input[name="options[element_wrapper_type_enable]"]' => ['checked' => TRUE],
690 '#fieldset' => 'style_settings',
693 $form['element_default_classes'] = [
694 '#type' => 'checkbox',
695 '#title' => $this->t('Add default classes'),
696 '#default_value' => $this->options['element_default_classes'],
697 '#description' => $this->t('Use default Views classes to identify the field, field label and field content.'),
698 '#fieldset' => 'style_settings',
702 '#title' => $this->t('Rewrite results'),
703 '#type' => 'details',
707 if ($this->allowAdvancedRender()) {
708 $form['alter']['#tree'] = TRUE;
709 $form['alter']['alter_text'] = [
710 '#type' => 'checkbox',
711 '#title' => $this->t('Override the output of this field with custom text'),
712 '#default_value' => $this->options['alter']['alter_text'],
715 $form['alter']['text'] = [
716 '#title' => $this->t('Text'),
717 '#type' => 'textarea',
718 '#default_value' => $this->options['alter']['text'],
719 '#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()]),
722 ':input[name="options[alter][alter_text]"]' => ['checked' => TRUE],
727 $form['alter']['make_link'] = [
728 '#type' => 'checkbox',
729 '#title' => $this->t('Output this field as a custom link'),
730 '#default_value' => $this->options['alter']['make_link'],
732 $form['alter']['path'] = [
733 '#title' => $this->t('Link path'),
734 '#type' => 'textfield',
735 '#default_value' => $this->options['alter']['path'],
736 '#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.'),
739 ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
744 $form['alter']['absolute'] = [
745 '#type' => 'checkbox',
746 '#title' => $this->t('Use absolute path'),
747 '#default_value' => $this->options['alter']['absolute'],
750 ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
754 $form['alter']['replace_spaces'] = [
755 '#type' => 'checkbox',
756 '#title' => $this->t('Replace spaces with dashes'),
757 '#default_value' => $this->options['alter']['replace_spaces'],
760 ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
764 $form['alter']['external'] = [
765 '#type' => 'checkbox',
766 '#title' => $this->t('External server URL'),
767 '#default_value' => $this->options['alter']['external'],
768 '#description' => $this->t("Links to an external server using a full URL: e.g. 'http://www.example.com' or 'www.example.com'."),
771 ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
775 $form['alter']['path_case'] = [
777 '#title' => $this->t('Transform the case'),
778 '#description' => $this->t('When printing URL paths, how to transform the case of the filter value.'),
781 ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
785 'none' => $this->t('No transform'),
786 'upper' => $this->t('Upper case'),
787 'lower' => $this->t('Lower case'),
788 'ucfirst' => $this->t('Capitalize first letter'),
789 'ucwords' => $this->t('Capitalize each word'),
791 '#default_value' => $this->options['alter']['path_case'],
793 $form['alter']['link_class'] = [
794 '#title' => $this->t('Link class'),
795 '#type' => 'textfield',
796 '#default_value' => $this->options['alter']['link_class'],
797 '#description' => $this->t('The CSS class to apply to the link.'),
800 ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
804 $form['alter']['alt'] = [
805 '#title' => $this->t('Title text'),
806 '#type' => 'textfield',
807 '#default_value' => $this->options['alter']['alt'],
808 '#description' => $this->t('Text to place as "title" text which most browsers display as a tooltip when hovering over the link.'),
811 ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
815 $form['alter']['rel'] = [
816 '#title' => $this->t('Rel Text'),
817 '#type' => 'textfield',
818 '#default_value' => $this->options['alter']['rel'],
819 '#description' => $this->t('Include Rel attribute for use in lightbox2 or other javascript utility.'),
822 ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
826 $form['alter']['prefix'] = [
827 '#title' => $this->t('Prefix text'),
828 '#type' => 'textfield',
829 '#default_value' => $this->options['alter']['prefix'],
830 '#description' => $this->t('Any text to display before this link. You may include HTML.'),
833 ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
837 $form['alter']['suffix'] = [
838 '#title' => $this->t('Suffix text'),
839 '#type' => 'textfield',
840 '#default_value' => $this->options['alter']['suffix'],
841 '#description' => $this->t('Any text to display after this link. You may include HTML.'),
844 ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
848 $form['alter']['target'] = [
849 '#title' => $this->t('Target'),
850 '#type' => 'textfield',
851 '#default_value' => $this->options['alter']['target'],
852 '#description' => $this->t("Target of the link, such as _blank, _parent or an iframe's name. This field is rarely used."),
855 ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
861 // Get a list of the available fields and arguments for token replacement.
863 // Setup the tokens for fields.
864 $previous = $this->getPreviousFieldLabels();
865 $optgroup_arguments = (string) t('Arguments');
866 $optgroup_fields = (string) t('Fields');
867 foreach ($previous as $id => $label) {
868 $options[$optgroup_fields]["{{ $id }}"] = substr(strrchr($label, ":"), 2 );
870 // Add the field to the list of options.
871 $options[$optgroup_fields]["{{ {$this->options['id']} }}"] = substr(strrchr($this->adminLabel(), ":"), 2 );
873 foreach ($this->view->display_handler->getHandlers('argument') as $arg => $handler) {
874 $options[$optgroup_arguments]["{{ arguments.$arg }}"] = $this->t('@argument title', ['@argument' => $handler->adminLabel()]);
875 $options[$optgroup_arguments]["{{ raw_arguments.$arg }}"] = $this->t('@argument input', ['@argument' => $handler->adminLabel()]);
878 $this->documentSelfTokens($options[$optgroup_fields]);
884 '#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>',
886 // We have some options, so make a list.
887 if (!empty($options)) {
889 '#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>',
891 foreach (array_keys($options) as $type) {
892 if (!empty($options[$type])) {
894 foreach ($options[$type] as $key => $value) {
895 $items[] = $key . ' == ' . $value;
898 '#theme' => 'item_list',
901 $output[] = $item_list;
905 // This construct uses 'hidden' and not markup because process doesn't
906 // run. It also has an extra div because the dependency wants to hide
907 // the parent in situations like this, so we need a second div to
909 $form['alter']['help'] = [
910 '#type' => 'details',
911 '#title' => $this->t('Replacement patterns'),
916 ':input[name="options[alter][make_link]"]' => ['checked' => TRUE],
919 ':input[name="options[alter][alter_text]"]' => ['checked' => TRUE],
922 ':input[name="options[alter][more_link]"]' => ['checked' => TRUE],
928 $form['alter']['trim'] = [
929 '#type' => 'checkbox',
930 '#title' => $this->t('Trim this field to a maximum number of characters'),
931 '#default_value' => $this->options['alter']['trim'],
934 $form['alter']['max_length'] = [
935 '#title' => $this->t('Maximum number of characters'),
936 '#type' => 'textfield',
937 '#default_value' => $this->options['alter']['max_length'],
940 ':input[name="options[alter][trim]"]' => ['checked' => TRUE],
945 $form['alter']['word_boundary'] = [
946 '#type' => 'checkbox',
947 '#title' => $this->t('Trim only on a word boundary'),
948 '#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.'),
949 '#default_value' => $this->options['alter']['word_boundary'],
952 ':input[name="options[alter][trim]"]' => ['checked' => TRUE],
957 $form['alter']['ellipsis'] = [
958 '#type' => 'checkbox',
959 '#title' => $this->t('Add "…" at the end of trimmed text'),
960 '#default_value' => $this->options['alter']['ellipsis'],
963 ':input[name="options[alter][trim]"]' => ['checked' => TRUE],
968 $form['alter']['more_link'] = [
969 '#type' => 'checkbox',
970 '#title' => $this->t('Add a read-more link if output is trimmed'),
971 '#default_value' => $this->options['alter']['more_link'],
974 ':input[name="options[alter][trim]"]' => ['checked' => TRUE],
979 $form['alter']['more_link_text'] = [
980 '#type' => 'textfield',
981 '#title' => $this->t('More link label'),
982 '#default_value' => $this->options['alter']['more_link_text'],
983 '#description' => $this->t('You may use the "Replacement patterns" above.'),
986 ':input[name="options[alter][trim]"]' => ['checked' => TRUE],
987 ':input[name="options[alter][more_link]"]' => ['checked' => TRUE],
991 $form['alter']['more_link_path'] = [
992 '#type' => 'textfield',
993 '#title' => $this->t('More link path'),
994 '#default_value' => $this->options['alter']['more_link_path'],
995 '#description' => $this->t('This can be an internal Drupal path such as node/add or an external URL such as "https://www.drupal.org". You may use the "Replacement patterns" above.'),
998 ':input[name="options[alter][trim]"]' => ['checked' => TRUE],
999 ':input[name="options[alter][more_link]"]' => ['checked' => TRUE],
1004 $form['alter']['html'] = [
1005 '#type' => 'checkbox',
1006 '#title' => $this->t('Field can contain HTML'),
1007 '#description' => $this->t('An HTML corrector will be run to ensure HTML tags are properly closed after trimming.'),
1008 '#default_value' => $this->options['alter']['html'],
1011 ':input[name="options[alter][trim]"]' => ['checked' => TRUE],
1016 $form['alter']['strip_tags'] = [
1017 '#type' => 'checkbox',
1018 '#title' => $this->t('Strip HTML tags'),
1019 '#default_value' => $this->options['alter']['strip_tags'],
1022 $form['alter']['preserve_tags'] = [
1023 '#type' => 'textfield',
1024 '#title' => $this->t('Preserve certain tags'),
1025 '#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'),
1026 '#default_value' => $this->options['alter']['preserve_tags'],
1029 ':input[name="options[alter][strip_tags]"]' => ['checked' => TRUE],
1034 $form['alter']['trim_whitespace'] = [
1035 '#type' => 'checkbox',
1036 '#title' => $this->t('Remove whitespace'),
1037 '#default_value' => $this->options['alter']['trim_whitespace'],
1040 $form['alter']['nl2br'] = [
1041 '#type' => 'checkbox',
1042 '#title' => $this->t('Convert newlines to HTML <br> tags'),
1043 '#default_value' => $this->options['alter']['nl2br'],
1047 $form['empty_field_behavior'] = [
1048 '#type' => 'details',
1049 '#title' => $this->t('No results behavior'),
1054 '#type' => 'textarea',
1055 '#title' => $this->t('No results text'),
1056 '#default_value' => $this->options['empty'],
1057 '#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.'),
1058 '#fieldset' => 'empty_field_behavior',
1061 $form['empty_zero'] = [
1062 '#type' => 'checkbox',
1063 '#title' => $this->t('Count the number 0 as empty'),
1064 '#default_value' => $this->options['empty_zero'],
1065 '#description' => $this->t('Enable to display the "no results text" if the field contains the number 0.'),
1066 '#fieldset' => 'empty_field_behavior',
1069 $form['hide_empty'] = [
1070 '#type' => 'checkbox',
1071 '#title' => $this->t('Hide if empty'),
1072 '#default_value' => $this->options['hide_empty'],
1073 '#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.'),
1074 '#fieldset' => 'empty_field_behavior',
1077 $form['hide_alter_empty'] = [
1078 '#type' => 'checkbox',
1079 '#title' => $this->t('Hide rewriting if empty'),
1080 '#default_value' => $this->options['hide_alter_empty'],
1081 '#description' => $this->t('Do not display rewritten content if this field is empty.'),
1082 '#fieldset' => 'empty_field_behavior',
1087 * Returns all field labels of fields before this field.
1090 * An array of field labels keyed by their field IDs.
1092 protected function getPreviousFieldLabels() {
1093 $all_fields = $this->view->display_handler->getFieldLabels();
1094 $field_options = array_slice($all_fields, 0, array_search($this->options['id'], array_keys($all_fields)));
1095 return $field_options;
1099 * Provide extra data to the administration form
1101 public function adminSummary() {
1102 return $this->label();
1108 public function preRender(&$values) { }
1113 public function render(ResultRow $values) {
1114 $value = $this->getValue($values);
1115 return $this->sanitizeValue($value);
1121 public function postRender(ResultRow $row, $output) {
1122 // Make sure the last rendered value is available also when this is
1123 // retrieved from cache.
1124 $this->last_render = $output;
1131 public function advancedRender(ResultRow $values) {
1132 // Clean up values from previous render calls.
1133 if ($this->lastRenderIndex != $values->index) {
1134 $this->last_render_text = '';
1136 if ($this->allowAdvancedRender() && $this instanceof MultiItemsFieldHandlerInterface) {
1137 $raw_items = $this->getItems($values);
1138 // If there are no items, set the original value to NULL.
1139 if (empty($raw_items)) {
1140 $this->original_value = NULL;
1144 $value = $this->render($values);
1145 if (is_array($value)) {
1146 $value = $this->getRenderer()->render($value);
1148 $this->last_render = $value;
1149 $this->original_value = $value;
1152 if ($this->allowAdvancedRender()) {
1154 if ($this instanceof MultiItemsFieldHandlerInterface) {
1156 foreach ($raw_items as $count => $item) {
1157 $value = $this->render_item($count, $item);
1158 if (is_array($value)) {
1159 $value = (string) $this->getRenderer()->render($value);
1161 $this->last_render = $value;
1162 $this->original_value = $this->last_render;
1164 $alter = $item + $this->options['alter'];
1165 $alter['phase'] = static::RENDER_TEXT_PHASE_SINGLE_ITEM;
1166 $items[] = $this->renderText($alter);
1169 $value = $this->renderItems($items);
1172 $alter = ['phase' => static::RENDER_TEXT_PHASE_COMPLETELY] + $this->options['alter'];
1173 $value = $this->renderText($alter);
1176 if (is_array($value)) {
1177 $value = $this->getRenderer()->render($value);
1179 // This happens here so that renderAsLink can get the unaltered value of
1180 // this field as a token rather than the altered value.
1181 $this->last_render = $value;
1184 // String cast is necessary to test emptiness of MarkupInterface
1186 if (empty((string) $this->last_render)) {
1187 if ($this->isValueEmpty($this->last_render, $this->options['empty_zero'], FALSE)) {
1188 $alter = $this->options['alter'];
1189 $alter['alter_text'] = 1;
1190 $alter['text'] = $this->options['empty'];
1191 $alter['phase'] = static::RENDER_TEXT_PHASE_EMPTY;
1192 $this->last_render = $this->renderText($alter);
1195 // If we rendered something, update the last render index.
1196 if ((string) $this->last_render !== '') {
1197 $this->lastRenderIndex = $values->index;
1199 return $this->last_render;
1205 public function isValueEmpty($value, $empty_zero, $no_skip_empty = TRUE) {
1206 // Convert MarkupInterface to a string for checking.
1207 if ($value instanceof MarkupInterface) {
1208 $value = (string) $value;
1210 if (!isset($value)) {
1214 $empty = ($empty_zero || ($value !== 0 && $value !== '0'));
1217 if ($no_skip_empty) {
1218 $empty = empty($value) && $empty;
1226 public function renderText($alter) {
1227 // We need to preserve the safeness of the value regardless of the
1228 // alterations made by this method. Any alterations or replacements made
1229 // within this method need to ensure that at the minimum the result is
1230 // XSS admin filtered. See self::renderAltered() as an example that does.
1231 $value_is_safe = $this->last_render instanceof MarkupInterface;
1232 // Cast to a string so that empty checks and string functions work as
1234 $value = (string) $this->last_render;
1236 if (!empty($alter['alter_text']) && $alter['text'] !== '') {
1237 $tokens = $this->getRenderTokens($alter);
1238 $value = $this->renderAltered($alter, $tokens);
1239 // $alter['text'] is entered through the views admin UI and will be safe
1240 // because the output of $this->renderAltered() is run through
1241 // Xss::filterAdmin().
1242 // @see \Drupal\views\Plugin\views\PluginBase::viewsTokenReplace()
1243 // @see \Drupal\Component\Utility\Xss::filterAdmin()
1244 $value_is_safe = TRUE;
1247 if (!empty($this->options['alter']['trim_whitespace'])) {
1248 $value = trim($value);
1251 // Check if there should be no further rewrite for empty values.
1252 $no_rewrite_for_empty = $this->options['hide_alter_empty'] && $this->isValueEmpty($this->original_value, $this->options['empty_zero']);
1254 // Check whether the value is empty and return nothing, so the field isn't rendered.
1255 // First check whether the field should be hidden if the value(hide_alter_empty = TRUE) /the rewrite is empty (hide_alter_empty = FALSE).
1256 // For numeric values you can specify whether "0"/0 should be empty.
1257 if ((($this->options['hide_empty'] && empty($value))
1258 || ($alter['phase'] != static::RENDER_TEXT_PHASE_EMPTY && $no_rewrite_for_empty))
1259 && $this->isValueEmpty($value, $this->options['empty_zero'], FALSE)) {
1262 // Only in empty phase.
1263 if ($alter['phase'] == static::RENDER_TEXT_PHASE_EMPTY && $no_rewrite_for_empty) {
1264 // If we got here then $alter contains the value of "No results text"
1265 // and so there is nothing left to do.
1266 return ViewsRenderPipelineMarkup::create($value);
1269 if (!empty($alter['strip_tags'])) {
1270 $value = strip_tags($value, $alter['preserve_tags']);
1274 if (!empty($alter['trim']) && !empty($alter['max_length'])) {
1275 $length = strlen($value);
1276 $value = $this->renderTrimText($alter, $value);
1277 if ($this->options['alter']['more_link'] && strlen($value) < $length) {
1278 $tokens = $this->getRenderTokens($alter);
1279 $more_link_text = $this->options['alter']['more_link_text'] ? $this->options['alter']['more_link_text'] : $this->t('more');
1280 $more_link_text = strtr(Xss::filterAdmin($more_link_text), $tokens);
1281 $more_link_path = $this->options['alter']['more_link_path'];
1282 $more_link_path = strip_tags(Html::decodeEntities($this->viewsTokenReplace($more_link_path, $tokens)));
1284 // Make sure that paths which were run through URL generation work as
1286 $base_path = base_path();
1287 // Checks whether the path starts with the base_path.
1288 if (strpos($more_link_path, $base_path) === 0) {
1289 $more_link_path = Unicode::substr($more_link_path, Unicode::strlen($base_path));
1292 // @todo Views should expect and store a leading /. See
1293 // https://www.drupal.org/node/2423913.
1301 if (UrlHelper::isExternal($more_link_path)) {
1302 $more_link_url = CoreUrl::fromUri($more_link_path, $options);
1305 $more_link_url = CoreUrl::fromUserInput('/' . $more_link_path, $options);
1307 $more_link = ' ' . $this->linkGenerator()->generate($more_link_text, $more_link_url);
1311 if (!empty($alter['nl2br'])) {
1312 $value = nl2br($value);
1315 if ($value_is_safe) {
1316 $value = ViewsRenderPipelineMarkup::create($value);
1318 $this->last_render_text = $value;
1320 if (!empty($alter['make_link']) && (!empty($alter['path']) || !empty($alter['url']))) {
1321 if (!isset($tokens)) {
1322 $tokens = $this->getRenderTokens($alter);
1324 $value = $this->renderAsLink($alter, $value, $tokens);
1327 // Preserve whether or not the string is safe. Since $more_link comes from
1328 // \Drupal::l(), it is safe to append. Check if the value is an instance of
1329 // \Drupal\Component\Render\MarkupInterface here because renderAsLink()
1330 // can return both safe and unsafe values.
1331 if ($value instanceof MarkupInterface) {
1332 return ViewsRenderPipelineMarkup::create($value . $more_link);
1335 // If the string is not already marked safe, it is still OK to return it
1336 // because it will be sanitized by Twig.
1337 return $value . $more_link;
1342 * Render this field as user-defined altered text.
1344 protected function renderAltered($alter, $tokens) {
1345 return $this->viewsTokenReplace($alter['text'], $tokens);
1349 * Trims the field down to the specified length.
1351 * @param array $alter
1352 * The alter array of options to use.
1353 * - max_length: Maximum length of the string, the rest gets truncated.
1354 * - word_boundary: Trim only on a word boundary.
1355 * - ellipsis: Show an ellipsis (…) at the end of the trimmed string.
1356 * - html: Make sure that the html is correct.
1358 * @param string $value
1359 * The string which should be trimmed.
1362 * The rendered trimmed string.
1364 protected function renderTrimText($alter, $value) {
1365 if (!empty($alter['strip_tags'])) {
1366 // NOTE: It's possible that some external fields might override the
1368 $this->definition['element type'] = 'span';
1370 return static::trimText($alter, $value);
1374 * Render this field as a link, with the info from a fieldset set by
1377 protected function renderAsLink($alter, $text, $tokens) {
1379 'absolute' => !empty($alter['absolute']) ? TRUE : FALSE,
1382 'entity_type' => NULL,
1392 $path = $alter['path'];
1393 // strip_tags() and viewsTokenReplace remove <front>, so check whether it's
1394 // different to front.
1395 if ($path != '<front>') {
1396 // Use strip_tags as there should never be HTML in the path.
1397 // However, we need to preserve special characters like " that were
1398 // removed by Html::escape().
1399 $path = Html::decodeEntities($this->viewsTokenReplace($alter['path'], $tokens));
1401 // Tokens might contain <front>, so check for <front> again.
1402 if ($path != '<front>') {
1403 $path = strip_tags($path);
1406 // Tokens might have resolved URL's, as is the case for tokens provided by
1407 // Link fields, so all internal paths will be prefixed by base_path(). For
1408 // proper further handling reset this to internal:/.
1409 if (strpos($path, base_path()) === 0) {
1410 $path = 'internal:/' . substr($path, strlen(base_path()));
1413 // If we have no $path and no $alter['url'], we have nothing to work with,
1414 // so we just return the text.
1415 if (empty($path) && empty($alter['url'])) {
1419 // If no scheme is provided in the $path, assign the default 'http://'.
1420 // This allows a url of 'www.example.com' to be converted to
1421 // 'http://www.example.com'.
1422 // Only do this when flag for external has been set, $path doesn't contain
1423 // a scheme and $path doesn't have a leading /.
1424 if ($alter['external'] && !parse_url($path, PHP_URL_SCHEME) && strpos($path, '/') !== 0) {
1425 // There is no scheme, add the default 'http://' to the $path.
1426 $path = "http://" . $path;
1430 if (empty($alter['url'])) {
1431 if (!parse_url($path, PHP_URL_SCHEME)) {
1432 // @todo Views should expect and store a leading /. See
1433 // https://www.drupal.org/node/2423913.
1434 $alter['url'] = CoreUrl::fromUserInput('/' . ltrim($path, '/'));
1437 $alter['url'] = CoreUrl::fromUri($path);
1441 $options = $alter['url']->getOptions() + $options;
1443 $path = $alter['url']->setOptions($options)->toUriString();
1445 if (!empty($alter['path_case']) && $alter['path_case'] != 'none' && !$alter['url']->isRouted()) {
1446 $path = str_replace($alter['path'], $this->caseTransform($alter['path'], $this->options['alter']['path_case']), $path);
1449 if (!empty($alter['replace_spaces'])) {
1450 $path = str_replace(' ', '-', $path);
1453 // Parse the URL and move any query and fragment parameters out of the path.
1454 $url = UrlHelper::parse($path);
1456 // Seriously malformed URLs may return FALSE or empty arrays.
1461 // If the path is empty do not build a link around the given text and return
1463 // http://www.example.com URLs will not have a $url['path'], so check host as well.
1464 if (empty($url['path']) && empty($url['host']) && empty($url['fragment']) && empty($url['url'])) {
1468 // If we get to here we have a path from the url parsing. So assign that to
1469 // $path now so we don't get query strings or fragments in the path.
1470 $path = $url['path'];
1472 if (isset($url['query'])) {
1473 // Remove query parameters that were assigned a query string replacement
1474 // token for which there is no value available.
1475 foreach ($url['query'] as $param => $val) {
1476 if ($val == '%' . $param) {
1477 unset($url['query'][$param]);
1479 // Replace any empty query params from URL parsing with NULL. So the
1480 // query will get built correctly with only the param key.
1481 // @see \Drupal\Component\Utility\UrlHelper::buildQuery().
1483 $url['query'][$param] = NULL;
1487 $options['query'] = $url['query'];
1490 if (isset($url['fragment'])) {
1491 $path = strtr($path, ['#' . $url['fragment'] => '']);
1492 // If the path is empty we want to have a fragment for the current site.
1494 $options['external'] = TRUE;
1496 $options['fragment'] = $url['fragment'];
1499 $alt = $this->viewsTokenReplace($alter['alt'], $tokens);
1500 // Set the title attribute of the link only if it improves accessibility
1501 if ($alt && $alt != $text) {
1502 $options['attributes']['title'] = Html::decodeEntities($alt);
1505 $class = $this->viewsTokenReplace($alter['link_class'], $tokens);
1507 $options['attributes']['class'] = [$class];
1510 if (!empty($alter['rel']) && $rel = $this->viewsTokenReplace($alter['rel'], $tokens)) {
1511 $options['attributes']['rel'] = $rel;
1514 $target = trim($this->viewsTokenReplace($alter['target'], $tokens));
1515 if (!empty($target)) {
1516 $options['attributes']['target'] = $target;
1519 // Allow the addition of arbitrary attributes to links. Additional attributes
1520 // currently can only be altered in preprocessors and not within the UI.
1521 if (isset($alter['link_attributes']) && is_array($alter['link_attributes'])) {
1522 foreach ($alter['link_attributes'] as $key => $attribute) {
1523 if (!isset($options['attributes'][$key])) {
1524 $options['attributes'][$key] = $this->viewsTokenReplace($attribute, $tokens);
1529 // If the query and fragment were programmatically assigned overwrite any
1531 if (isset($alter['query'])) {
1532 // Convert the query to a string, perform token replacement, and then
1533 // convert back to an array form for
1534 // \Drupal\Core\Utility\LinkGeneratorInterface::generate().
1535 $options['query'] = UrlHelper::buildQuery($alter['query']);
1536 $options['query'] = $this->viewsTokenReplace($options['query'], $tokens);
1538 parse_str($options['query'], $query);
1539 $options['query'] = $query;
1541 if (isset($alter['alias'])) {
1542 // Alias is a boolean field, so no token.
1543 $options['alias'] = $alter['alias'];
1545 if (isset($alter['fragment'])) {
1546 $options['fragment'] = $this->viewsTokenReplace($alter['fragment'], $tokens);
1548 if (isset($alter['language'])) {
1549 $options['language'] = $alter['language'];
1552 // If the url came from entity_uri(), pass along the required options.
1553 if (isset($alter['entity'])) {
1554 $options['entity'] = $alter['entity'];
1556 if (isset($alter['entity_type'])) {
1557 $options['entity_type'] = $alter['entity_type'];
1560 // The path has been heavily processed above, so it should be used as-is.
1561 $final_url = CoreUrl::fromUri($path, $options);
1563 // Build the link based on our altered Url object, adding on the optional
1564 // prefix and suffix
1568 '#url' => $final_url,
1571 if (!empty($alter['prefix'])) {
1572 $render['#prefix'] = $this->viewsTokenReplace($alter['prefix'], $tokens);
1574 if (!empty($alter['suffix'])) {
1575 $render['#suffix'] = $this->viewsTokenReplace($alter['suffix'], $tokens);
1577 return $this->getRenderer()->render($render);
1584 public function getRenderTokens($item) {
1586 if (!empty($this->view->build_info['substitutions'])) {
1587 $tokens = $this->view->build_info['substitutions'];
1590 foreach ($this->displayHandler->getHandlers('argument') as $arg => $handler) {
1591 $token = "{{ arguments.$arg }}";
1592 if (!isset($tokens[$token])) {
1593 $tokens[$token] = '';
1596 // Use strip tags as there should never be HTML in the path.
1597 // However, we need to preserve special characters like " that
1598 // were removed by Html::escape().
1599 $tokens["{{ raw_arguments.$arg }}"] = isset($this->view->args[$count]) ? strip_tags(Html::decodeEntities($this->view->args[$count])) : '';
1603 // Get flattened set of tokens for any array depth in query parameters.
1604 if ($request = $this->view->getRequest()) {
1605 $tokens += $this->getTokenValuesRecursive($request->query->all());
1608 // Now add replacements for our fields.
1609 foreach ($this->displayHandler->getHandlers('field') as $field => $handler) {
1610 /** @var static $handler */
1611 $placeholder = $handler->getFieldTokenPlaceholder();
1613 if (isset($handler->last_render)) {
1614 $tokens[$placeholder] = $handler->last_render;
1617 $tokens[$placeholder] = '';
1620 // We only use fields up to (and including) this one.
1621 if ($field == $this->options['id']) {
1626 // Store the tokens for the row so we can reference them later if necessary.
1627 $this->view->style_plugin->render_tokens[$this->view->row_index] = $tokens;
1628 $this->last_tokens = $tokens;
1629 if (!empty($item)) {
1630 $this->addSelfTokens($tokens, $item);
1637 * Returns a token placeholder for the current field.
1640 * A token placeholder.
1642 protected function getFieldTokenPlaceholder() {
1643 return '{{ ' . $this->options['id'] . ' }}';
1647 * Recursive function to add replacements for nested query string parameters.
1649 * E.g. if you pass in the following array:
1663 * Would yield the following array of tokens:
1665 * '%foo_a' => 'value'
1666 * '%foo_b' => 'value'
1667 * '%bar_a' => 'value'
1668 * '%bar_b_c' => 'value'
1672 * An array of values.
1674 * @param $parent_keys
1675 * An array of parent keys. This will represent the array depth.
1678 * An array of available tokens, with nested keys representative of the array structure.
1680 protected function getTokenValuesRecursive(array $array, array $parent_keys = []) {
1683 foreach ($array as $param => $val) {
1684 if (is_array($val)) {
1685 // Copy parent_keys array, so we don't affect other elements of this
1687 $child_parent_keys = $parent_keys;
1688 $child_parent_keys[] = $param;
1689 // Get the child tokens.
1690 $child_tokens = $this->getTokenValuesRecursive($val, $child_parent_keys);
1691 // Add them to the current tokens array.
1692 $tokens += $child_tokens;
1695 // Create a token key based on array element structure.
1696 $token_string = !empty($parent_keys) ? implode('.', $parent_keys) . '.' . $param : $param;
1697 $tokens['{{ arguments.' . $token_string . ' }}'] = strip_tags(Html::decodeEntities($val));
1705 * Add any special tokens this field might use for itself.
1707 * This method is intended to be overridden by items that generate
1708 * fields as a list. For example, the field that displays all terms
1709 * on a node might have tokens for the tid and the term.
1711 * By convention, tokens should follow the format of {{ token__subtoken }}
1712 * where token is the field ID and subtoken is the field. If the
1713 * field ID is terms, then the tokens might be {{ terms__tid }} and
1714 * {{ terms__name }}.
1716 protected function addSelfTokens(&$tokens, $item) { }
1719 * Document any special tokens this field might use for itself.
1721 * @see addSelfTokens()
1723 protected function documentSelfTokens(&$tokens) { }
1728 public function theme(ResultRow $values) {
1729 $renderer = $this->getRenderer();
1731 '#theme' => $this->themeFunctions(),
1732 '#view' => $this->view,
1736 $output = $renderer->render($build);
1738 // Set the bubbleable rendering metadata on $view->element. This ensures the
1739 // bubbleable rendering metadata of individual rendered fields is not lost.
1740 // @see \Drupal\Core\Render\Renderer::updateStack()
1741 $this->view->element = $renderer->mergeBubbleableMetadata($this->view->element, $build);
1746 public function themeFunctions() {
1748 $hook = 'views_view_field';
1750 $display = $this->view->display_handler->display;
1752 if (!empty($display)) {
1753 $themes[] = $hook . '__' . $this->view->storage->id() . '__' . $display['id'] . '__' . $this->options['id'];
1754 $themes[] = $hook . '__' . $this->view->storage->id() . '__' . $display['id'];
1755 $themes[] = $hook . '__' . $display['id'] . '__' . $this->options['id'];
1756 $themes[] = $hook . '__' . $display['id'];
1757 if ($display['id'] != $display['display_plugin']) {
1758 $themes[] = $hook . '__' . $this->view->storage->id() . '__' . $display['display_plugin'] . '__' . $this->options['id'];
1759 $themes[] = $hook . '__' . $this->view->storage->id() . '__' . $display['display_plugin'];
1760 $themes[] = $hook . '__' . $display['display_plugin'] . '__' . $this->options['id'];
1761 $themes[] = $hook . '__' . $display['display_plugin'];
1764 $themes[] = $hook . '__' . $this->view->storage->id() . '__' . $this->options['id'];
1765 $themes[] = $hook . '__' . $this->view->storage->id();
1766 $themes[] = $hook . '__' . $this->options['id'];
1772 public function adminLabel($short = FALSE) {
1773 return $this->getField(parent::adminLabel($short));
1777 * Trims the field down to the specified length.
1779 * @param array $alter
1780 * The alter array of options to use.
1781 * - max_length: Maximum length of the string, the rest gets truncated.
1782 * - word_boundary: Trim only on a word boundary.
1783 * - ellipsis: Show an ellipsis (…) at the end of the trimmed string.
1784 * - html: Make sure that the html is correct.
1786 * @param string $value
1787 * The string which should be trimmed.
1790 * The trimmed string.
1792 public static function trimText($alter, $value) {
1793 if (Unicode::strlen($value) > $alter['max_length']) {
1794 $value = Unicode::substr($value, 0, $alter['max_length']);
1795 if (!empty($alter['word_boundary'])) {
1796 $regex = "(.*)\b.+";
1797 if (function_exists('mb_ereg')) {
1798 mb_regex_encoding('UTF-8');
1799 $found = mb_ereg($regex, $value, $matches);
1802 $found = preg_match("/$regex/us", $value, $matches);
1805 $value = $matches[1];
1808 // Remove scraps of HTML entities from the end of a strings
1809 $value = rtrim(preg_replace('/(?:<(?!.+>)|&(?!.+;)).*$/us', '', $value));
1811 if (!empty($alter['ellipsis'])) {
1815 if (!empty($alter['html'])) {
1816 $value = Html::normalize($value);
1823 * Gets the link generator.
1825 * @return \Drupal\Core\Utility\LinkGeneratorInterface
1827 protected function linkGenerator() {
1828 if (!isset($this->linkGenerator)) {
1829 $this->linkGenerator = \Drupal::linkGenerator();
1831 return $this->linkGenerator;
1835 * Returns the render API renderer.
1837 * @return \Drupal\Core\Render\RendererInterface
1839 protected function getRenderer() {
1840 if (!isset($this->renderer)) {
1841 $this->renderer = \Drupal::service('renderer');
1844 return $this->renderer;
1850 * @} End of "defgroup views_field_handlers".