3 namespace Drupal\views\Plugin\views\style;
5 use Drupal\Component\Utility\Html;
6 use Drupal\Component\Utility\Xss;
7 use Drupal\Core\Form\FormStateInterface;
8 use Drupal\views\Plugin\views\display\DisplayPluginBase;
9 use Drupal\views\Plugin\views\PluginBase;
10 use Drupal\views\Plugin\views\wizard\WizardInterface;
11 use Drupal\views\Render\ViewsRenderPipelineMarkup;
12 use Drupal\views\ViewExecutable;
15 * @defgroup views_style_plugins Views style plugins
17 * Plugins that control how the collection of results is rendered in a view.
19 * Style plugins control how a view is displayed. For the most part, they are
20 * object wrappers around theme templates. Examples of styles include HTML
21 * lists, tables, full or teaser content views, etc.
23 * Many (but not all) style plugins have an optional row plugin, which
24 * displays a single record. Not all style plugins use row plugins, so it is
25 * up to the style plugin to set this up and call the row plugin. See the
26 * @link views_row_plugins Views row plugins topic @endlink for more
29 * Style plugins extend \Drupal\views\Plugin\views\style\StylePluginBase. They
30 * must be annotated with \Drupal\views\Annotation\ViewsStyle
31 * annotation, and they must be in namespace directory Plugin\views\style.
33 * @ingroup views_plugins
38 * Base class for views style plugins.
40 abstract class StylePluginBase extends PluginBase {
45 protected $usesOptions = TRUE;
48 * Store all available tokens row rows.
50 protected $rowTokens = [];
53 * Does the style plugin allows to use style plugins.
57 protected $usesRowPlugin = FALSE;
60 * Does the style plugin support custom css class for the rows.
64 protected $usesRowClass = FALSE;
67 * Does the style plugin support grouping of rows.
71 protected $usesGrouping = TRUE;
74 * Does the style plugin for itself support to add fields to it's output.
76 * This option only makes sense on style plugins without row plugins, like
81 protected $usesFields = FALSE;
84 * Stores the rendered field values, keyed by the row index and field name.
86 * @see \Drupal\views\Plugin\views\style\StylePluginBase::renderFields()
87 * @see \Drupal\views\Plugin\views\style\StylePluginBase::getField()
91 protected $rendered_fields;
94 * The theme function used to render the grouping set.
96 * Plugins may override this attribute if they wish to use some other theme
97 * function to render the grouping set.
101 * @see StylePluginBase::renderGroupingSets()
103 protected $groupingTheme = 'views_view_grouping';
106 * Should field labels be enabled by default.
110 protected $defaultFieldLabels = FALSE;
113 * Overrides \Drupal\views\Plugin\views\PluginBase::init().
115 * The style options might come externally as the style can be sourced from at
116 * least two locations. If it's not included, look on the display.
118 public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
119 parent::init($view, $display, $options);
121 if ($this->usesRowPlugin() && $display->getOption('row')) {
122 $this->view->rowPlugin = $display->getPlugin('row');
134 public function destroy() {
137 if (isset($this->view->rowPlugin)) {
138 $this->view->rowPlugin->destroy();
143 * Returns the usesRowPlugin property.
147 public function usesRowPlugin() {
148 return $this->usesRowPlugin;
153 * Returns the usesRowClass property.
157 public function usesRowClass() {
158 return $this->usesRowClass;
162 * Returns the usesGrouping property.
166 public function usesGrouping() {
167 return $this->usesGrouping;
171 * Return TRUE if this style also uses fields.
175 public function usesFields() {
176 // If we use a row plugin, ask the row plugin. Chances are, we don't
178 $row_uses_fields = FALSE;
179 if ($this->usesRowPlugin() && ($row_plugin = $this->displayHandler->getPlugin('row'))) {
180 $row_uses_fields = $row_plugin->usesFields();
182 // Otherwise, check the definition or the option.
183 return $row_uses_fields || $this->usesFields || !empty($this->options['uses_fields']);
187 * Return TRUE if this style uses tokens.
189 * Used to ensure we don't fetch tokens when not needed for performance.
191 public function usesTokens() {
192 if ($this->usesRowClass()) {
193 $class = $this->options['row_class'];
194 if (strpos($class, '{{') !== FALSE) {
201 * Return TRUE if this style enables field labels by default.
205 public function defaultFieldLabels() {
206 return $this->defaultFieldLabels;
210 * Return the token replaced row class for the specified row.
212 public function getRowClass($row_index) {
213 if ($this->usesRowClass()) {
214 $class = $this->options['row_class'];
215 if ($this->usesFields() && $this->view->field) {
216 $class = strip_tags($this->tokenizeValue($class, $row_index));
219 $classes = explode(' ', $class);
220 foreach ($classes as &$class) {
221 $class = Html::cleanCssIdentifier($class);
223 return implode(' ', $classes);
228 * Take a value and apply token replacement logic to it.
230 public function tokenizeValue($value, $row_index) {
231 if (strpos($value, '{{') !== FALSE) {
232 // Row tokens might be empty, for example for node row style.
233 $tokens = isset($this->rowTokens[$row_index]) ? $this->rowTokens[$row_index] : [];
234 if (!empty($this->view->build_info['substitutions'])) {
235 $tokens += $this->view->build_info['substitutions'];
238 $value = $this->viewsTokenReplace($value, $tokens);
241 // ::viewsTokenReplace() will run Xss::filterAdmin on the
242 // resulting string. We do the same here for consistency.
243 $value = Xss::filterAdmin($value);
249 * Should the output of the style plugin be rendered even if it's a empty view.
251 public function evenEmpty() {
252 return !empty($this->definition['even empty']);
258 protected function defineOptions() {
259 $options = parent::defineOptions();
260 $options['grouping'] = ['default' => []];
261 if ($this->usesRowClass()) {
262 $options['row_class'] = ['default' => ''];
263 $options['default_row_class'] = ['default' => TRUE];
265 $options['uses_fields'] = ['default' => FALSE];
273 public function buildOptionsForm(&$form, FormStateInterface $form_state) {
274 parent::buildOptionsForm($form, $form_state);
275 // Only fields-based views can handle grouping. Style plugins can also exclude
276 // themselves from being groupable by setting their "usesGrouping" property
278 // @TODO: Document "usesGrouping" in docs.php when docs.php is written.
279 if ($this->usesFields() && $this->usesGrouping()) {
280 $options = ['' => $this->t('- None -')];
281 $field_labels = $this->displayHandler->getFieldLabels(TRUE);
282 $options += $field_labels;
283 // If there are no fields, we can't group on them.
284 if (count($options) > 1) {
285 // This is for backward compatibility, when there was just a single
287 if (is_string($this->options['grouping'])) {
288 $grouping = $this->options['grouping'];
289 $this->options['grouping'] = [];
290 $this->options['grouping'][0]['field'] = $grouping;
292 if (isset($this->options['group_rendered']) && is_string($this->options['group_rendered'])) {
293 $this->options['grouping'][0]['rendered'] = $this->options['group_rendered'];
294 unset($this->options['group_rendered']);
297 $c = count($this->options['grouping']);
298 // Add a form for every grouping, plus one.
299 for ($i = 0; $i <= $c; $i++) {
300 $grouping = !empty($this->options['grouping'][$i]) ? $this->options['grouping'][$i] : [];
301 $grouping += ['field' => '', 'rendered' => TRUE, 'rendered_strip' => FALSE];
302 $form['grouping'][$i]['field'] = [
304 '#title' => $this->t('Grouping field Nr.@number', ['@number' => $i + 1]),
305 '#options' => $options,
306 '#default_value' => $grouping['field'],
307 '#description' => $this->t('You may optionally specify a field by which to group the records. Leave blank to not group.'),
309 $form['grouping'][$i]['rendered'] = [
310 '#type' => 'checkbox',
311 '#title' => $this->t('Use rendered output to group rows'),
312 '#default_value' => $grouping['rendered'],
313 '#description' => $this->t('If enabled the rendered output of the grouping field is used to group the rows.'),
316 ':input[name="style_options[grouping][' . $i . '][field]"]' => ['value' => ''],
320 $form['grouping'][$i]['rendered_strip'] = [
321 '#type' => 'checkbox',
322 '#title' => $this->t('Remove tags from rendered output'),
323 '#default_value' => $grouping['rendered_strip'],
326 ':input[name="style_options[grouping][' . $i . '][field]"]' => ['value' => ''],
334 if ($this->usesRowClass()) {
335 $form['row_class'] = [
336 '#title' => $this->t('Row class'),
337 '#description' => $this->t('The class to provide on each row.'),
338 '#type' => 'textfield',
339 '#default_value' => $this->options['row_class'],
342 if ($this->usesFields()) {
343 $form['row_class']['#description'] .= ' ' . $this->t('You may use field tokens from as per the "Replacement patterns" used in "Rewrite the output of this field" for all fields.');
346 $form['default_row_class'] = [
347 '#title' => $this->t('Add views row classes'),
348 '#description' => $this->t('Add the default row classes like @classes to the output. You can use this to quickly reduce the amount of markup the view provides by default, at the cost of making it more difficult to apply CSS.', ['@classes' => 'views-row']),
349 '#type' => 'checkbox',
350 '#default_value' => $this->options['default_row_class'],
354 if (!$this->usesFields() || !empty($this->options['uses_fields'])) {
355 $form['uses_fields'] = [
356 '#type' => 'checkbox',
357 '#title' => $this->t('Force using fields'),
358 '#description' => $this->t('If neither the row nor the style plugin supports fields, this field allows to enable them, so you can for example use groupby.'),
359 '#default_value' => $this->options['uses_fields'],
367 public function validateOptionsForm(&$form, FormStateInterface $form_state) {
368 // Don't run validation on style plugins without the grouping setting.
369 if ($form_state->hasValue(['style_options', 'grouping'])) {
370 // Don't save grouping if no field is specified.
371 $groupings = $form_state->getValue(['style_options', 'grouping']);
372 foreach ($groupings as $index => $grouping) {
373 if (empty($grouping['field'])) {
374 $form_state->unsetValue(['style_options', 'grouping', $index]);
381 * Provide a form in the views wizard if this style is selected.
384 * An associative array containing the structure of the form.
385 * @param \Drupal\Core\Form\FormStateInterface $form_state
386 * The current state of the form.
387 * @param string $type
388 * The display type, either block or page.
390 public function wizardForm(&$form, FormStateInterface $form_state, $type) {
394 * Alter the options of a display before they are added to the view.
397 * An associative array containing the structure of the form.
398 * @param \Drupal\Core\Form\FormStateInterface $form_state
399 * The current state of the form.
400 * @param \Drupal\views\Plugin\views\wizard\WizardInterface $wizard
401 * The current used wizard.
402 * @param array $display_options
403 * The options which will be used on the view. The style plugin should
404 * alter this to its own needs.
405 * @param string $display_type
406 * The display type, either block or page.
408 public function wizardSubmit(&$form, FormStateInterface $form_state, WizardInterface $wizard, &$display_options, $display_type) {
412 * Called by the view builder to see if this style handler wants to
413 * interfere with the sorts. If so it should build; if it returns
414 * any non-TRUE value, normal sorting will NOT be added to the query.
416 public function buildSort() { return TRUE; }
419 * Called by the view builder to let the style build a second set of
420 * sorts that will come after any other sorts in the view.
422 public function buildSortPost() { }
425 * Allow the style to do stuff before each row is rendered.
428 * The full array of results from the query.
430 public function preRender($result) {
431 if (!empty($this->view->rowPlugin)) {
432 $this->view->rowPlugin->preRender($result);
437 * Renders a group of rows of the grouped view.
440 * The result rows rendered in this group.
443 * The render array containing the single group theme output.
445 protected function renderRowGroup(array $rows = []) {
447 '#theme' => $this->themeFunctions(),
448 '#view' => $this->view,
454 * Render the display in this style.
456 public function render() {
457 if ($this->usesRowPlugin() && empty($this->view->rowPlugin)) {
458 debug('Drupal\views\Plugin\views\style\StylePluginBase: Missing row plugin');
462 // Group the rows according to the grouping instructions, if specified.
463 $sets = $this->renderGrouping(
465 $this->options['grouping'],
469 return $this->renderGroupingSets($sets);
473 * Render the grouping sets.
475 * Plugins may override this method if they wish some other way of handling
479 * An array keyed by group content containing the grouping sets to render.
480 * Each set contains the following associative array:
481 * - group: The group content.
482 * - level: The hierarchical level of the grouping.
483 * - rows: The result rows to be rendered in this group..
486 * Render array of grouping sets.
488 public function renderGroupingSets($sets) {
490 $theme_functions = $this->view->buildThemeFunctions($this->groupingTheme);
491 foreach ($sets as $set) {
492 $level = isset($set['level']) ? $set['level'] : 0;
494 $row = reset($set['rows']);
495 // Render as a grouping set.
496 if (is_array($row) && isset($row['group'])) {
498 '#theme' => $theme_functions,
499 '#view' => $this->view,
500 '#grouping' => $this->options['grouping'][$level],
501 '#rows' => $set['rows'],
504 // Render as a record set.
506 if ($this->usesRowPlugin()) {
507 foreach ($set['rows'] as $index => $row) {
508 $this->view->row_index = $index;
509 $set['rows'][$index] = $this->view->rowPlugin->render($row);
513 $single_output = $this->renderRowGroup($set['rows']);
516 $single_output['#grouping_level'] = $level;
517 $single_output['#title'] = $set['group'];
518 $output[] = $single_output;
520 unset($this->view->row_index);
525 * Group records as needed for rendering.
528 * An array of records from the view to group.
530 * An array of grouping instructions on which fields to group. If empty, the
531 * result set will be given a single group with an empty string as a label.
532 * @param $group_rendered
533 * Boolean value whether to use the rendered or the raw field value for
534 * grouping. If set to NULL the return is structured as before
535 * Views 7.x-3.0-rc2. After Views 7.x-3.0 this boolean is only used if
536 * $groupings is an old-style string or if the rendered option is missing
537 * for a grouping instruction.
539 * The grouped record set.
540 * A nested set structure is generated if multiple grouping fields are used.
544 * 'grouping_field_1:grouping_1' => array(
545 * 'group' => 'grouping_field_1:content_1',
548 * 'grouping_field_2:grouping_a' => array(
549 * 'group' => 'grouping_field_2:content_a',
552 * $row_index_1 => $row_1,
553 * $row_index_2 => $row_2,
559 * 'grouping_field_1:grouping_2' => array(
565 public function renderGrouping($records, $groupings = [], $group_rendered = NULL) {
566 // This is for backward compatibility, when $groupings was a string
567 // containing the ID of a single field.
568 if (is_string($groupings)) {
569 $rendered = $group_rendered === NULL ? TRUE : $group_rendered;
570 $groupings = [['field' => $groupings, 'rendered' => $rendered]];
573 // Make sure fields are rendered
574 $this->renderFields($this->view->result);
577 foreach ($records as $index => $row) {
578 // Iterate through configured grouping fields to determine the
579 // hierarchically positioned set where the current row belongs to.
580 // While iterating, parent groups, that do not exist yet, are added.
582 foreach ($groupings as $level => $info) {
583 $field = $info['field'];
584 $rendered = isset($info['rendered']) ? $info['rendered'] : $group_rendered;
585 $rendered_strip = isset($info['rendered_strip']) ? $info['rendered_strip'] : FALSE;
588 // Group on the rendered version of the field, not the raw. That way,
589 // we can control any special formatting of the grouping field through
590 // the admin or theme layer or anywhere else we'd like.
591 if (isset($this->view->field[$field])) {
592 $group_content = $this->getField($index, $field);
593 if ($this->view->field[$field]->options['label']) {
594 $group_content = $this->view->field[$field]->options['label'] . ': ' . $group_content;
597 $grouping = (string) $group_content;
598 if ($rendered_strip) {
599 $group_content = $grouping = strip_tags(htmlspecialchars_decode($group_content));
603 $grouping = $this->getFieldValue($index, $field);
604 // Not all field handlers return a scalar value,
605 // e.g. views_handler_field_field.
606 if (!is_scalar($grouping)) {
607 $grouping = hash('sha256', serialize($grouping));
612 // Create the group if it does not exist yet.
613 if (empty($set[$grouping])) {
614 $set[$grouping]['group'] = $group_content;
615 $set[$grouping]['level'] = $level;
616 $set[$grouping]['rows'] = [];
619 // Move the set reference into the row set of the group we just determined.
620 $set = &$set[$grouping]['rows'];
622 // Add the row to the hierarchically positioned row set we just determined.
627 // Create a single group with an empty grouping field.
634 // If this parameter isn't explicitly set, modify the output to be fully
635 // backward compatible to code before Views 7.x-3.0-rc2.
636 // @TODO Remove this as soon as possible e.g. October 2020
637 if ($group_rendered === NULL) {
638 $old_style_sets = [];
639 foreach ($sets as $group) {
640 $old_style_sets[$group['group']] = $group['rows'];
642 $sets = $old_style_sets;
649 * Renders all of the fields for a given style and store them on the object.
651 * @param array $result
652 * The result array from $view->result
654 protected function renderFields(array $result) {
655 if (!$this->usesFields()) {
659 if (!isset($this->rendered_fields)) {
660 $this->rendered_fields = [];
661 $this->view->row_index = 0;
662 $field_ids = array_keys($this->view->field);
664 // Only tokens relating to field handlers preceding the one we invoke
665 // ::getRenderTokens() on are returned, so here we need to pick the last
666 // available field handler.
667 $render_tokens_field_id = end($field_ids);
669 // If all fields have a field::access FALSE there might be no fields, so
670 // there is no reason to execute this code.
671 if (!empty($field_ids)) {
672 $renderer = $this->getRenderer();
673 /** @var \Drupal\views\Plugin\views\cache\CachePluginBase $cache_plugin */
674 $cache_plugin = $this->view->display_handler->getPlugin('cache');
675 $max_age = $cache_plugin->getCacheMaxAge();
677 /** @var \Drupal\views\ResultRow $row */
678 foreach ($result as $index => $row) {
679 $this->view->row_index = $index;
681 // Here we implement render caching for result rows. Since we never
682 // build a render array for single rows, given that style templates
683 // need individual field markup to support proper theming, we build
684 // a raw render array containing all field render arrays and cache it.
685 // This allows us to cache the markup of the various children, that is
686 // individual fields, which is then available for style template
687 // preprocess functions, later in the rendering workflow.
688 // @todo Fetch all the available cached row items in one single cache
689 // get operation, once https://www.drupal.org/node/2453945 is fixed.
691 '#pre_render' => [[$this, 'elementPreRenderRow']],
694 'keys' => $cache_plugin->getRowCacheKeys($row),
695 'tags' => $cache_plugin->getRowCacheTags($row),
696 'max-age' => $max_age,
698 '#cache_properties' => $field_ids,
700 $renderer->addCacheableDependency($data, $this->view->storage);
701 // Views may be rendered both inside and outside a render context:
702 // - HTML views are rendered inside a render context: then we want to
703 // use ::render(), so that attachments and cacheability are bubbled.
704 // - non-HTML views are rendered outside a render context: then we
705 // want to use ::renderPlain(), so that no bubbling happens
706 if ($renderer->hasRenderContext()) {
707 $renderer->render($data);
710 $renderer->renderPlain($data);
713 // Extract field output from the render array and post process it.
714 $fields = $this->view->field;
715 $rendered_fields = &$this->rendered_fields[$index];
716 $post_render_tokens = [];
717 foreach ($field_ids as $id) {
718 $rendered_fields[$id] = $data[$id]['#markup'];
719 $tokens = $fields[$id]->postRender($row, $rendered_fields[$id]);
721 $post_render_tokens += $tokens;
725 // Populate row tokens.
726 $this->rowTokens[$index] = $this->view->field[$render_tokens_field_id]->getRenderTokens([]);
728 // Replace post-render tokens.
729 if ($post_render_tokens) {
730 $placeholders = array_keys($post_render_tokens);
731 $values = array_values($post_render_tokens);
732 foreach ($this->rendered_fields[$index] as &$rendered_field) {
733 // Placeholders and rendered fields have been processed by the
734 // render system and are therefore safe.
735 $rendered_field = ViewsRenderPipelineMarkup::create(str_replace($placeholders, $values, $rendered_field));
741 unset($this->view->row_index);
746 * #pre_render callback for view row field rendering.
748 * @see self::render()
751 * The element to #pre_render
754 * The processed element.
756 public function elementPreRenderRow(array $data) {
757 // Render row fields.
758 foreach ($this->view->field as $id => $field) {
759 $data[$id] = ['#markup' => $field->theme($data['#row'])];
765 * Gets a rendered field.
768 * The index count of the row.
769 * @param string $field
770 * The ID of the field.
772 * @return \Drupal\Component\Render\MarkupInterface|null
773 * The output of the field, or NULL if it was empty.
775 public function getField($index, $field) {
776 if (!isset($this->rendered_fields)) {
777 $this->renderFields($this->view->result);
780 if (isset($this->rendered_fields[$index][$field])) {
781 return $this->rendered_fields[$index][$field];
786 * Get the raw field value.
789 * The index count of the row.
791 * The id of the field.
793 public function getFieldValue($index, $field) {
794 $this->view->row_index = $index;
795 $value = $this->view->field[$field]->getValue($this->view->result[$index]);
796 unset($this->view->row_index);
803 public function validate() {
804 $errors = parent::validate();
806 if ($this->usesRowPlugin()) {
807 $plugin = $this->displayHandler->getPlugin('row');
808 if (empty($plugin)) {
809 $errors[] = $this->t('Style @style requires a row style but the row plugin is invalid.', ['@style' => $this->definition['title']]);
812 $result = $plugin->validate();
813 if (!empty($result) && is_array($result)) {
814 $errors = array_merge($errors, $result);
824 public function query() {
826 if (isset($this->view->rowPlugin)) {
827 $this->view->rowPlugin->query();