5 * Preprocessors and helper functions to make theming easier.
8 use Drupal\Component\Utility\Html;
9 use Drupal\Component\Utility\Xss;
10 use Drupal\Core\Template\Attribute;
14 * Prepares variables for view templates.
16 * Default template: views-view.html.twig.
18 * @param array $variables
19 * An associative array containing:
20 * - view: The ViewExecutable object.
22 function template_preprocess_views_view(&$variables) {
23 $view = $variables['view'];
24 $id = $view->storage->id();
26 $variables['css_name'] = Html::cleanCssIdentifier($id);
27 $variables['id'] = $id;
28 $variables['display_id'] = $view->current_display;
29 // Override the title to be empty by default. For example, if viewing a page
30 // view, 'title' will already be populated in $variables. This can still be
31 // overridden to use a title when needed. See views_ui_preprocess_views_view()
32 // for an example of this.
33 $variables['title'] = '';
35 $css_class = $view->display_handler->getOption('css_class');
36 if (!empty($css_class)) {
37 // Views uses its own sanitization method. This is preserved to keep
38 // backwards compatibility.
39 // @todo https://www.drupal.org/project/drupal/issues/2977950 Decide what to
40 // do with the backwards compatibility layer.
41 $bc_classes = explode(' ', preg_replace('/[^a-zA-Z0-9- ]/', '-', $css_class));
42 // Sanitize the classes using the classes using the proper API.
43 $sanitized_classes = array_map('\Drupal\Component\Utility\Html::cleanCssIdentifier', explode(' ', $css_class));
44 $view_classes = array_unique(array_merge($bc_classes, $sanitized_classes));
45 // Merge the view display classes into any existing classes if they exist.
46 $variables['attributes']['class'] = !empty($variables['attributes']['class']) ? array_merge($variables['attributes']['class'], $view_classes) : $view_classes;
47 $variables['css_class'] = implode(' ', $view_classes);
50 // contextual_preprocess() only works on render elements, and since this theme
51 // hook is not for a render element, contextual_preprocess() falls back to the
52 // first argument and checks if that is a render element. The first element is
53 // view_array. However, view_array does not get set anywhere, but since we do
54 // have access to the View object, we can also access the View object's
55 // element, which is a render element that does have #contextual_links set if
56 // the display supports it. Doing this allows contextual_preprocess() to
57 // access this theme hook's render element, and therefore allows this template
58 // to have contextual links.
60 $variables['view_array'] = $variables['view']->element;
62 // Attachments are always updated with the outer view, never by themselves,
63 // so they do not have dom ids.
64 if (empty($view->is_attachment)) {
65 // Our JavaScript needs to have some means to find the HTML belonging to
68 // It is true that the DIV wrapper has classes denoting the name of the view
69 // and its display ID, but this is not enough to unequivocally match a view
70 // with its HTML, because one view may appear several times on the page. So
71 // we set up a hash with the current time, $dom_id, to issue a "unique"
72 // identifier for each view. This identifier is written to both
73 // drupalSettings and the DIV wrapper.
74 $variables['dom_id'] = $view->dom_id;
79 * Prepares variables for views fields templates.
81 * Default template: views-view-fields.html.twig.
83 * @param array $variables
84 * An associative array containing:
85 * - view: The view object.
86 * - options: An array of options. Each option contains:
87 * - inline: An array that contains the fields that are to be
89 * - default_field_elements: If default field wrapper
90 * elements are to be provided.
91 * - hide_empty: Whether the field is to be hidden if empty.
92 * - element_default_classes: If the default classes are to be added.
93 * - separator: A string to be placed between inline fields to keep them
95 * - row: An array containing information about the current row.
97 function template_preprocess_views_view_fields(&$variables) {
98 $view = $variables['view'];
100 // Loop through the fields for this view.
101 $previous_inline = FALSE;
102 // Ensure it's at least an empty array.
103 $variables['fields'] = [];
104 /** @var \Drupal\views\ResultRow $row */
105 $row = $variables['row'];
106 foreach ($view->field as $id => $field) {
107 // render this even if set to exclude so it can be used elsewhere.
108 $field_output = $view->style_plugin->getField($row->index, $id);
109 $empty = $field->isValueEmpty($field_output, $field->options['empty_zero']);
110 if (empty($field->options['exclude']) && (!$empty || (empty($field->options['hide_empty']) && empty($variables['options']['hide_empty'])))) {
111 $object = new stdClass();
112 $object->handler = $view->field[$id];
113 $object->inline = !empty($variables['options']['inline'][$id]);
114 // Set up default value of the flag that indicates whether to display a
115 // colon after the label.
116 $object->has_label_colon = FALSE;
118 $object->element_type = $object->handler->elementType(TRUE, !$variables['options']['default_field_elements'], $object->inline);
119 if ($object->element_type) {
121 if ($object->handler->options['element_default_classes']) {
122 $attributes['class'][] = 'field-content';
125 if ($classes = $object->handler->elementClasses($row->index)) {
126 $attributes['class'][] = $classes;
128 $object->element_attributes = new Attribute($attributes);
131 $object->content = $field_output;
132 if (isset($view->field[$id]->field_alias) && isset($row->{$view->field[$id]->field_alias})) {
133 $object->raw = $row->{$view->field[$id]->field_alias};
136 // Make sure it exists to reduce NOTICE.
140 if (!empty($variables['options']['separator']) && $previous_inline && $object->inline && $object->content) {
141 $object->separator = Xss::filterAdmin($variables['options']['separator']);
144 $object->class = Html::cleanCssIdentifier($id);
146 $previous_inline = $object->inline;
147 // Set up field wrapper element.
148 $object->wrapper_element = $object->handler->elementWrapperType(TRUE, TRUE);
149 if ($object->wrapper_element === '' && $variables['options']['default_field_elements']) {
150 $object->wrapper_element = $object->inline ? 'span' : 'div';
153 // Set up field wrapper attributes if field wrapper was set.
154 if ($object->wrapper_element) {
156 if ($object->handler->options['element_default_classes']) {
157 $attributes['class'][] = 'views-field';
158 $attributes['class'][] = 'views-field-' . $object->class;
161 if ($classes = $object->handler->elementWrapperClasses($row->index)) {
162 $attributes['class'][] = $classes;
164 $object->wrapper_attributes = new Attribute($attributes);
167 // Set up field label
168 $object->label = $view->field[$id]->label();
170 // Set up field label wrapper and its attributes.
171 if ($object->label) {
172 // Add a colon in a label suffix.
173 if ($object->handler->options['element_label_colon']) {
174 $object->label_suffix = ': ';
175 $object->has_label_colon = TRUE;
178 // Set up label HTML element.
179 $object->label_element = $object->handler->elementLabelType(TRUE, !$variables['options']['default_field_elements']);
181 // Set up label attributes.
182 if ($object->label_element) {
184 if ($object->handler->options['element_default_classes']) {
185 $attributes['class'][] = 'views-label';
186 $attributes['class'][] = 'views-label-' . $object->class;
189 // Set up field label.
190 $element_label_class = $object->handler->elementLabelClasses($row->index);
191 if ($element_label_class) {
192 $attributes['class'][] = $element_label_class;
194 $object->label_attributes = new Attribute($attributes);
198 $variables['fields'][$id] = $object;
205 * Prepares variables for views single grouping templates.
207 * Default template: views-view-grouping.html.twig.
209 * @param array $variables
210 * An associative array containing:
211 * - view: The view object.
212 * - rows: The rows returned from the view.
213 * - grouping_level: Integer indicating the hierarchical level of the
215 * - content: The content to be grouped.
216 * - title: The group heading.
218 function template_preprocess_views_view_grouping(&$variables) {
219 $variables['content'] = $variables['view']->style_plugin->renderGroupingSets($variables['rows'], $variables['grouping_level']);
223 * Prepares variables for views field templates.
225 * Default template: views-view-field.html.twig.
227 * @param array $variables
228 * An associative array containing:
229 * - field: The field handler object for the current field.
230 * - row: Object representing the raw result of the SQL query for the current
232 * - view: Instance of the ViewExecutable object for the parent view.
234 function template_preprocess_views_view_field(&$variables) {
235 $variables['output'] = $variables['field']->advancedRender($variables['row']);
239 * Prepares variables for views summary templates.
241 * The summary prints a single record from a row, with fields.
243 * Default template: views-view-summary.html.twig.
245 * @param array $variables
246 * An associative array containing:
247 * - view: A ViewExecutable object.
248 * - rows: The raw row data.
250 function template_preprocess_views_view_summary(&$variables) {
251 /** @var \Drupal\views\ViewExecutable $view */
252 $view = $variables['view'];
253 $argument = $view->argument[$view->build_info['summary_level']];
257 if (!empty($view->exposed_raw_input)) {
258 $url_options['query'] = $view->exposed_raw_input;
262 // Force system path.
263 \Drupal::url('<current>', [], ['alias' => TRUE]),
264 // Force system path.
265 Url::fromRouteMatch(\Drupal::routeMatch())->setOption('alias', TRUE)->toString(),
266 // Could be an alias.
267 \Drupal::url('<current>'),
268 // Could be an alias.
269 Url::fromRouteMatch(\Drupal::routeMatch())->toString(),
271 $active_urls = array_combine($active_urls, $active_urls);
273 // Collect all arguments foreach row, to be able to alter them for example
274 // by the validator. This is not done per single argument value, because this
275 // could cause performance problems.
278 foreach ($variables['rows'] as $id => $row) {
279 $row_args[$id] = $argument->summaryArgument($row);
281 $argument->processSummaryArguments($row_args);
283 foreach ($variables['rows'] as $id => $row) {
284 $variables['rows'][$id]->attributes = [];
285 $variables['rows'][$id]->link = $argument->summaryName($row);
287 $args[$argument->position] = $row_args[$id];
289 if (!empty($argument->options['summary_options']['base_path'])) {
290 $base_path = $argument->options['summary_options']['base_path'];
291 $tokens = $view->getDisplay()->getArgumentsTokens();
292 $base_path = $argument->globalTokenReplace($base_path, $tokens);
293 // @todo Views should expect and store a leading /. See:
294 // https://www.drupal.org/node/2423913
295 $url = Url::fromUserInput('/' . $base_path);
297 /** @var \Symfony\Component\Routing\Route $route */
298 $route_name = $url->getRouteName();
299 $route = \Drupal::service('router.route_provider')->getRouteByName($route_name);
301 $route_variables = $route->compile()->getVariables();
302 $parameters = $url->getRouteParameters();
304 foreach ($route_variables as $variable_name) {
305 $parameters[$variable_name] = array_shift($args);
308 $url->setRouteParameters($parameters);
310 catch (Exception $e) {
311 // If the given route doesn't exist, default to <front>
312 $url = Url::fromRoute('<front>');
316 $url = $view->getUrl($args)->setOptions($url_options);
318 $variables['rows'][$id]->url = $url->toString();
319 $variables['rows'][$id]->count = intval($row->{$argument->count_alias});
320 $variables['rows'][$id]->attributes = new Attribute($variables['rows'][$id]->attributes);
321 $variables['rows'][$id]->active = isset($active_urls[$variables['rows'][$id]->url]);
326 * Prepares variables for unformatted summary view templates.
328 * Default template: views-view-summary-unformatted.html.twig.
330 * @param array $variables
331 * An associative array containing:
332 * - view: A ViewExecutable object.
333 * - rows: The raw row data.
334 * - options: An array of options. Each option contains:
335 * - separator: A string to be placed between inline fields to keep them
338 function template_preprocess_views_view_summary_unformatted(&$variables) {
339 /** @var \Drupal\views\ViewExecutable $view */
340 $view = $variables['view'];
341 $argument = $view->argument[$view->build_info['summary_level']];
345 if (!empty($view->exposed_raw_input)) {
346 $url_options['query'] = $view->exposed_raw_input;
351 // Force system path.
352 \Drupal::url('<current>', [], ['alias' => TRUE]),
353 // Could be an alias.
354 \Drupal::url('<current>'),
356 $active_urls = array_combine($active_urls, $active_urls);
358 // Collect all arguments for each row, to be able to alter them for example
359 // by the validator. This is not done per single argument value, because
360 // this could cause performance problems.
362 foreach ($variables['rows'] as $id => $row) {
363 $row_args[$id] = $argument->summaryArgument($row);
365 $argument->processSummaryArguments($row_args);
367 foreach ($variables['rows'] as $id => $row) {
368 // Only false on first time.
370 $variables['rows'][$id]->separator = Xss::filterAdmin($variables['options']['separator']);
372 $variables['rows'][$id]->attributes = [];
373 $variables['rows'][$id]->link = $argument->summaryName($row);
375 $args[$argument->position] = $row_args[$id];
377 if (!empty($argument->options['summary_options']['base_path'])) {
378 $base_path = $argument->options['summary_options']['base_path'];
379 $tokens = $view->getDisplay()->getArgumentsTokens();
380 $base_path = $argument->globalTokenReplace($base_path, $tokens);
381 // @todo Views should expect and store a leading /. See:
382 // https://www.drupal.org/node/2423913
383 $url = Url::fromUserInput('/' . $base_path);
385 /** @var \Symfony\Component\Routing\Route $route */
386 $route = \Drupal::service('router.route_provider')->getRouteByName($url->getRouteName());
387 $route_variables = $route->compile()->getVariables();
388 $parameters = $url->getRouteParameters();
390 foreach ($route_variables as $variable_name) {
391 $parameters[$variable_name] = array_shift($args);
394 $url->setRouteParameters($parameters);
396 catch (Exception $e) {
397 // If the given route doesn't exist, default to <front>
398 $url = Url::fromRoute('<front>');
402 $url = $view->getUrl($args)->setOptions($url_options);
404 $variables['rows'][$id]->url = $url->toString();
405 $variables['rows'][$id]->count = intval($row->{$argument->count_alias});
406 $variables['rows'][$id]->active = isset($active_urls[$variables['rows'][$id]->url]);
407 $variables['rows'][$id]->attributes = new Attribute($variables['rows'][$id]->attributes);
412 * Prepares variables for views table templates.
414 * Default template: views-view-table.html.twig.
416 * @param array $variables
417 * An associative array containing:
418 * - view: A ViewExecutable object.
419 * - rows: The raw row data.
421 function template_preprocess_views_view_table(&$variables) {
422 $view = $variables['view'];
424 // We need the raw data for this grouping, which is passed in
425 // as $variables['rows'].
426 // However, the template also needs to use for the rendered fields. We
427 // therefore swap the raw data out to a new variable and reset $variables['rows']
428 // so that it can get rebuilt.
429 // Store rows so that they may be used by further preprocess functions.
430 $result = $variables['result'] = $variables['rows'];
431 $variables['rows'] = [];
432 $variables['header'] = [];
434 $options = $view->style_plugin->options;
435 $handler = $view->style_plugin;
437 $fields = &$view->field;
438 $columns = $handler->sanitizeColumns($options['columns'], $fields);
440 $active = !empty($handler->active) ? $handler->active : '';
441 $order = !empty($handler->order) ? $handler->order : 'asc';
443 // A boolean variable which stores whether the table has a responsive class.
446 // For the actual site we want to not render full URLs, because this would
447 // make pagers cacheable per URL, which is problematic in blocks, for example.
448 // For the actual live preview though the javascript relies on properly
450 $route_name = !empty($view->live_preview) ? '<current>' : '<none>';
452 $query = tablesort_get_query_parameters();
453 if (isset($view->exposed_raw_input)) {
454 $query += $view->exposed_raw_input;
457 // A boolean to store whether the table's header has any labels.
458 $has_header_labels = FALSE;
459 foreach ($columns as $field => $column) {
460 // Create a second variable so we can easily find what fields we have and
461 // what the CSS classes should be.
462 $variables['fields'][$field] = Html::cleanCssIdentifier($field);
463 if ($active == $field) {
464 $variables['fields'][$field] .= ' is-active';
467 // Render the header labels.
468 if ($field == $column && empty($fields[$field]->options['exclude'])) {
469 $label = !empty($fields[$field]) ? $fields[$field]->label() : '';
470 if (empty($options['info'][$field]['sortable']) || !$fields[$field]->clickSortable()) {
471 $variables['header'][$field]['content'] = $label;
474 $initial = !empty($options['info'][$field]['default_sort_order']) ? $options['info'][$field]['default_sort_order'] : 'asc';
476 if ($active == $field) {
477 $initial = ($order == 'asc') ? 'desc' : 'asc';
480 $title = t('sort by @s', ['@s' => $label]);
481 if ($active == $field) {
482 $variables['header'][$field]['sort_indicator'] = [
483 '#theme' => 'tablesort_indicator',
484 '#style' => $initial,
488 $query['order'] = $field;
489 $query['sort'] = $initial;
493 $url = new Url($route_name, [], $link_options);
494 $variables['header'][$field]['url'] = $url->toString();
495 $variables['header'][$field]['content'] = $label;
496 $variables['header'][$field]['title'] = $title;
499 $variables['header'][$field]['default_classes'] = $fields[$field]->options['element_default_classes'];
500 // Set up the header label class.
501 $variables['header'][$field]['attributes'] = [];
502 $class = $fields[$field]->elementLabelClasses(0);
504 $variables['header'][$field]['attributes']['class'][] = $class;
506 // Add responsive header classes.
507 if (!empty($options['info'][$field]['responsive'])) {
508 $variables['header'][$field]['attributes']['class'][] = $options['info'][$field]['responsive'];
511 // Add a CSS align class to each field if one was set.
512 if (!empty($options['info'][$field]['align'])) {
513 $variables['header'][$field]['attributes']['class'][] = Html::cleanCssIdentifier($options['info'][$field]['align']);
515 // Add a header label wrapper if one was selected.
516 if ($variables['header'][$field]['content']) {
517 $element_label_type = $fields[$field]->elementLabelType(TRUE, TRUE);
518 if ($element_label_type) {
519 $variables['header'][$field]['wrapper_element'] = $element_label_type;
521 // Improves accessibility of complex tables.
522 $variables['header'][$field]['attributes']['id'] = Html::getUniqueId('view-' . $field . '-table-column');
524 // Check if header label is not empty.
525 if (!empty($variables['header'][$field]['content'])) {
526 $has_header_labels = TRUE;
529 $variables['header'][$field]['attributes'] = new Attribute($variables['header'][$field]['attributes']);
532 // Add a CSS align class to each field if one was set.
533 if (!empty($options['info'][$field]['align'])) {
534 $variables['fields'][$field] .= ' ' . Html::cleanCssIdentifier($options['info'][$field]['align']);
537 // Render each field into its appropriate column.
538 foreach ($result as $num => $row) {
540 // Skip building the attributes and content if the field is to be excluded
542 if (!empty($fields[$field]->options['exclude'])) {
546 // Reference to the column in the loop to make the code easier to read.
547 $column_reference =& $variables['rows'][$num]['columns'][$column];
549 $column_reference['default_classes'] = $fields[$field]->options['element_default_classes'];
551 // Set the field key to the column so it can be used for adding classes
553 $column_reference['fields'][] = $variables['fields'][$field];
555 // Add field classes.
556 if (!isset($column_reference['attributes'])) {
557 $column_reference['attributes'] = [];
560 if ($classes = $fields[$field]->elementClasses($num)) {
561 $column_reference['attributes']['class'][] = $classes;
564 // Add responsive header classes.
565 if (!empty($options['info'][$field]['responsive'])) {
566 $column_reference['attributes']['class'][] = $options['info'][$field]['responsive'];
569 // Improves accessibility of complex tables.
570 if (isset($variables['header'][$field]['attributes']['id'])) {
571 $column_reference['attributes']['headers'] = [$variables['header'][$field]['attributes']['id']];
574 if (!empty($fields[$field])) {
575 $field_output = $handler->getField($num, $field);
576 $column_reference['wrapper_element'] = $fields[$field]->elementType(TRUE, TRUE);
577 if (!isset($column_reference['content'])) {
578 $column_reference['content'] = [];
581 // Only bother with separators and stuff if the field shows up.
582 // Place the field into the column, along with an optional separator.
583 if (trim($field_output) != '') {
584 if (!empty($column_reference['content']) && !empty($options['info'][$column]['separator'])) {
585 $column_reference['content'][] = [
586 'separator' => ['#markup' => $options['info'][$column]['separator']],
587 'field_output' => ['#markup' => $field_output],
591 $column_reference['content'][] = [
592 'field_output' => ['#markup' => $field_output],
597 $column_reference['attributes'] = new Attribute($column_reference['attributes']);
600 // Remove columns if the "empty_column" option is checked and the
602 if (!empty($options['info'][$field]['empty_column'])) {
604 foreach ($variables['rows'] as $columns) {
605 $empty &= empty($columns['columns'][$column]['content']);
608 foreach ($variables['rows'] as &$column_items) {
609 unset($column_items['columns'][$column]);
611 unset($variables['header'][$column]);
616 // Hide table header if all labels are empty.
617 if (!$has_header_labels) {
618 $variables['header'] = [];
621 foreach ($variables['rows'] as $num => $row) {
622 $variables['rows'][$num]['attributes'] = [];
623 if ($row_class = $handler->getRowClass($num)) {
624 $variables['rows'][$num]['attributes']['class'][] = $row_class;
626 $variables['rows'][$num]['attributes'] = new Attribute($variables['rows'][$num]['attributes']);
629 if (empty($variables['rows']) && !empty($options['empty_table'])) {
630 $build = $view->display_handler->renderArea('empty');
631 $variables['rows'][0]['columns'][0]['content'][0]['field_output'] = $build;
632 $variables['rows'][0]['attributes'] = new Attribute(['class' => ['odd']]);
633 // Calculate the amounts of rows with output.
634 $variables['rows'][0]['columns'][0]['attributes'] = new Attribute([
635 'colspan' => count($variables['header']),
636 'class' => ['views-empty'],
640 $variables['sticky'] = FALSE;
641 if (!empty($options['sticky'])) {
642 $variables['view']->element['#attached']['library'][] = 'core/drupal.tableheader';
643 $variables['sticky'] = TRUE;
646 // Add the caption to the list if set.
647 if (!empty($handler->options['caption'])) {
648 $variables['caption'] = ['#markup' => $handler->options['caption']];
649 $variables['caption_needed'] = TRUE;
651 elseif (!empty($variables['title'])) {
652 $variables['caption'] = ['#markup' => $variables['title']];
653 $variables['caption_needed'] = TRUE;
656 $variables['caption'] = '';
657 $variables['caption_needed'] = FALSE;
660 $variables['summary'] = $handler->options['summary'];
661 $variables['description'] = $handler->options['description'];
662 $variables['caption_needed'] |= !empty($variables['summary']) || !empty($variables['description']);
664 $variables['responsive'] = FALSE;
665 // If the table has headers and it should react responsively to columns hidden
666 // with the classes represented by the constants RESPONSIVE_PRIORITY_MEDIUM
667 // and RESPONSIVE_PRIORITY_LOW, add the tableresponsive behaviors.
668 if (isset($variables['header']) && $responsive) {
669 $variables['view']->element['#attached']['library'][] = 'core/drupal.tableresponsive';
670 // Add 'responsive-enabled' class to the table to identify it for JS.
671 // This is needed to target tables constructed by this function.
672 $variables['responsive'] = TRUE;
677 * Prepares variables for views grid style templates.
679 * Default template: views-view-grid.html.twig.
681 * @param array $variables
682 * An associative array containing:
683 * - view: The view object.
684 * - rows: An array of row items. Each row is an array of content.
686 function template_preprocess_views_view_grid(&$variables) {
687 $options = $variables['options'] = $variables['view']->style_plugin->options;
688 $horizontal = ($options['alignment'] === 'horizontal');
693 $remainders = count($variables['rows']) % $options['columns'];
694 $num_rows = floor(count($variables['rows']) / $options['columns']);
696 // Iterate over each rendered views result row.
697 foreach ($variables['rows'] as $result_index => $item) {
701 $items[$row]['content'][$col]['content'] = $item;
704 $items[$col]['content'][$row]['content'] = $item;
707 // Create attributes for rows.
708 if (!$horizontal || ($horizontal && empty($items[$row]['attributes']))) {
709 $row_attributes = ['class' => []];
710 // Add custom row classes.
711 $row_class = array_filter(explode(' ', $variables['view']->style_plugin->getCustomClass($result_index, 'row')));
712 if (!empty($row_class)) {
713 $row_attributes['class'] = array_merge($row_attributes['class'], $row_class);
715 // Add row attributes to the item.
717 $items[$row]['attributes'] = new Attribute($row_attributes);
720 $items[$col]['content'][$row]['attributes'] = new Attribute($row_attributes);
724 // Create attributes for columns.
725 if ($horizontal || (!$horizontal && empty($items[$col]['attributes']))) {
726 $col_attributes = ['class' => []];
727 // Add default views column classes.
728 // Add custom column classes.
729 $col_class = array_filter(explode(' ', $variables['view']->style_plugin->getCustomClass($result_index, 'col')));
730 if (!empty($col_class)) {
731 $col_attributes['class'] = array_merge($col_attributes['class'], $col_class);
733 // Add automatic width for columns.
734 if ($options['automatic_width']) {
735 $col_attributes['style'] = 'width: ' . (100 / $options['columns']) . '%;';
737 // Add column attributes to the item.
739 $items[$row]['content'][$col]['attributes'] = new Attribute($col_attributes);
742 $items[$col]['attributes'] = new Attribute($col_attributes);
746 // Increase, decrease or reset appropriate integers.
748 if ($col == 0 && $col != ($options['columns'] - 1)) {
751 elseif ($col >= ($options['columns'] - 1)) {
761 if (!$remainders && $row == $num_rows) {
765 elseif ($remainders && $row == $num_rows + 1) {
773 // Add items to the variables array.
774 $variables['items'] = $items;
778 * Prepares variables for views unformatted rows templates.
780 * Default template: views-view-unformatted.html.twig.
782 * @param array $variables
783 * An associative array containing:
784 * - view: The view object.
785 * - rows: An array of row items. Each row is an array of content.
787 function template_preprocess_views_view_unformatted(&$variables) {
788 $view = $variables['view'];
789 $rows = $variables['rows'];
790 $style = $view->style_plugin;
791 $options = $style->options;
793 $variables['default_row_class'] = !empty($options['default_row_class']);
794 foreach ($rows as $id => $row) {
795 $variables['rows'][$id] = [];
796 $variables['rows'][$id]['content'] = $row;
797 $variables['rows'][$id]['attributes'] = new Attribute();
798 if ($row_class = $view->style_plugin->getRowClass($id)) {
799 $variables['rows'][$id]['attributes']->addClass($row_class);
805 * Prepares variables for Views HTML list templates.
807 * Default template: views-view-list.html.twig.
809 * @param array $variables
810 * An associative array containing:
811 * - view: A View object.
813 function template_preprocess_views_view_list(&$variables) {
814 $handler = $variables['view']->style_plugin;
816 // Fetch classes from handler options.
817 if ($handler->options['class']) {
818 $class = explode(' ', $handler->options['class']);
819 $class = array_map('\Drupal\Component\Utility\Html::cleanCssIdentifier', $class);
821 // Initialize a new attribute class for $class.
822 $variables['list']['attributes'] = new Attribute(['class' => $class]);
825 // Fetch wrapper classes from handler options.
826 if ($handler->options['wrapper_class']) {
827 $wrapper_class = explode(' ', $handler->options['wrapper_class']);
828 $variables['attributes']['class'] = array_map('\Drupal\Component\Utility\Html::cleanCssIdentifier', $wrapper_class);
831 $variables['list']['type'] = $handler->options['type'];
833 template_preprocess_views_view_unformatted($variables);
837 * Prepares variables for RSS feed templates.
839 * Default template: views-view-rss.html.twig.
841 * @param array $variables
842 * An associative array containing:
843 * - view: A ViewExecutable object.
844 * - rows: The raw row data.
846 function template_preprocess_views_view_rss(&$variables) {
847 $view = $variables['view'];
848 $items = $variables['rows'];
849 $style = $view->style_plugin;
851 $config = \Drupal::config('system.site');
853 // The RSS 2.0 "spec" doesn't indicate HTML can be used in the description.
854 // We strip all HTML tags, but need to prevent double encoding from properly
855 // escaped source data (such as & becoming &amp;).
856 $variables['description'] = Html::decodeEntities(strip_tags($style->getDescription()));
858 if ($view->display_handler->getOption('sitename_title')) {
859 $title = $config->get('name');
860 if ($slogan = $config->get('slogan')) {
861 $title .= ' - ' . $slogan;
865 $title = $view->getTitle();
867 $variables['title'] = $title;
869 // Figure out which display which has a path we're using for this feed. If
870 // there isn't one, use the global $base_url
871 $link_display_id = $view->display_handler->getLinkDisplay();
872 /** @var \Drupal\views\Plugin\views\display\DisplayPluginBase $display */
873 if ($link_display_id && ($display = $view->displayHandlers->get($link_display_id)) && $display->isEnabled()) {
874 $url = $view->getUrl(NULL, $link_display_id);
877 /** @var \Drupal\Core\Url $url */
879 $url_options = ['absolute' => TRUE];
880 if (!empty($view->exposed_raw_input)) {
881 $url_options['query'] = $view->exposed_raw_input;
884 // Compare the link to the default home page; if it's the default home page,
885 // just use $base_url.
886 $url_string = $url->setOptions($url_options)->toString();
887 if ($url_string === Url::fromUserInput($config->get('page.front'))->toString()) {
888 $url_string = Url::fromRoute('<front>')->setAbsolute()->toString();
891 $variables['link'] = $url_string;
894 $variables['langcode'] = \Drupal::languageManager()->getCurrentLanguage()->getId();
895 $variables['namespaces'] = new Attribute($style->namespaces);
896 $variables['items'] = $items;
897 $variables['channel_elements'] = \Drupal::service('renderer')->render($style->channel_elements);
899 // During live preview we don't want to output the header since the contents
900 // of the feed are being displayed inside a normal HTML page.
901 if (empty($variables['view']->live_preview)) {
902 $variables['view']->getResponse()->headers->set('Content-Type', 'application/rss+xml; charset=utf-8');
907 * Prepares variables for views RSS item templates.
909 * Default template: views-view-row-rss.html.twig.
911 * @param array $variables
912 * An associative array containing:
913 * - row: The raw results rows.
915 function template_preprocess_views_view_row_rss(&$variables) {
916 $item = $variables['row'];
917 $variables['title'] = $item->title;
918 $variables['link'] = $item->link;
920 // The description is the only place where we should find HTML.
921 // @see https://validator.w3.org/feed/docs/rss2.html#hrelementsOfLtitemgt
922 // If we have a render array, render it here and pass the result to the
923 // template, letting Twig autoescape it.
924 if (isset($item->description) && is_array($item->description)) {
925 $variables['description'] = (string) \Drupal::service('renderer')->render($item->description);
928 $variables['item_elements'] = [];
929 foreach ($item->elements as $element) {
930 if (isset($element['attributes']) && is_array($element['attributes'])) {
931 $element['attributes'] = new Attribute($element['attributes']);
933 $variables['item_elements'][] = $element;
938 * Prepares variables for OPML feed templates.
940 * Default template: views-view-opml.html.twig.
942 * @param array $variables
943 * An associative array containing:
944 * - view: A ViewExecutable object.
945 * - rows: The raw row data.
947 function template_preprocess_views_view_opml(&$variables) {
948 $view = $variables['view'];
949 $items = $variables['rows'];
951 $config = \Drupal::config('system.site');
953 if ($view->display_handler->getOption('sitename_title')) {
954 $title = $config->get('name');
955 if ($slogan = $config->get('slogan')) {
956 $title .= ' - ' . $slogan;
960 $title = $view->getTitle();
962 $variables['title'] = $title;
963 $variables['items'] = $items;
964 $variables['updated'] = gmdate(DATE_RFC2822, REQUEST_TIME);
966 // During live preview we don't want to output the header since the contents
967 // of the feed are being displayed inside a normal HTML page.
968 if (empty($variables['view']->live_preview)) {
969 $variables['view']->getResponse()->headers->set('Content-Type', 'text/xml; charset=utf-8');
974 * Prepares variables for views OPML item templates.
976 * Default template: views-view-row-opml.html.twig.
978 * @param array $variables
979 * An associative array containing:
980 * - row: The raw results rows.
982 function template_preprocess_views_view_row_opml(&$variables) {
983 $item = $variables['row'];
985 $variables['attributes'] = new Attribute($item);
989 * Prepares variables for views exposed form templates.
991 * Default template: views-exposed-form.html.twig.
993 * @param array $variables
994 * An associative array containing:
995 * - form: A render element representing the form.
997 function template_preprocess_views_exposed_form(&$variables) {
998 $form = &$variables['form'];
1000 if (!empty($form['q'])) {
1001 $variables['q'] = $form['q'];
1004 foreach ($form['#info'] as $info) {
1005 if (!empty($info['label'])) {
1006 $form[$info['value']]['#title'] = $info['label'];
1008 if (!empty($info['description'])) {
1009 $form[$info['value']]['#description'] = $info['description'];
1015 * Prepares variables for views mini-pager templates.
1017 * Default template: views-mini-pager.html.twig.
1019 * @param array $variables
1020 * An associative array containing:
1021 * - tags: Provides link text for the next/previous links.
1022 * - element: The pager's id.
1023 * - parameters: Any extra GET parameters that should be retained, such as
1026 function template_preprocess_views_mini_pager(&$variables) {
1027 global $pager_page_array, $pager_total;
1029 $tags = &$variables['tags'];
1030 $element = $variables['element'];
1031 $parameters = $variables['parameters'];
1033 // Current is the page we are currently paged to.
1034 $variables['items']['current'] = $pager_page_array[$element] + 1;
1036 if ($pager_total[$element] > 1 && $pager_page_array[$element] > 0) {
1038 'query' => pager_query_add_page($parameters, $element, $pager_page_array[$element] - 1),
1040 $variables['items']['previous']['href'] = \Drupal::url('<current>', [], $options);
1041 if (isset($tags[1])) {
1042 $variables['items']['previous']['text'] = $tags[1];
1044 $variables['items']['previous']['attributes'] = new Attribute();
1047 if ($pager_page_array[$element] < ($pager_total[$element] - 1)) {
1049 'query' => pager_query_add_page($parameters, $element, $pager_page_array[$element] + 1),
1051 $variables['items']['next']['href'] = \Drupal::url('<current>', [], $options);
1052 if (isset($tags[3])) {
1053 $variables['items']['next']['text'] = $tags[3];
1055 $variables['items']['next']['attributes'] = new Attribute();
1058 // This is based on the entire current query string. We need to ensure
1059 // cacheability is affected accordingly.
1060 $variables['#cache']['contexts'][] = 'url.query_args';
1064 * @defgroup views_templates Views template files
1066 * Describes various views templates & overriding options.
1068 * All views templates can be overridden with a variety of names, using
1069 * the view, the display ID of the view, the display type of the view,
1070 * or some combination thereof.
1072 * For each view, there will be a minimum of two templates used. The first
1073 * is used for all views: views-view.html.twig.
1075 * The second template is determined by the style selected for the view. Note
1076 * that certain aspects of the view can also change which style is used; for
1077 * example, arguments which provide a summary view might change the style to
1078 * one of the special summary styles.
1080 * The default style for all views is views-view-unformatted.html.twig.
1082 * Many styles will then farm out the actual display of each row to a row
1083 * style; the default row style is views-view-fields.html.twig.
1085 * Here is an example of all the templates that will be tried in the following
1088 * View, named foobar. Style: unformatted. Row style: Fields. Display: Page.
1090 * - views-view--foobar--page.html.twig
1091 * - views-view--page.html.twig
1092 * - views-view--foobar.html.twig
1093 * - views-view.html.twig
1095 * - views-view-unformatted--foobar--page.html.twig
1096 * - views-view-unformatted--page.html.twig
1097 * - views-view-unformatted--foobar.html.twig
1098 * - views-view-unformatted.html.twig
1100 * - views-view-fields--foobar--page.html.twig
1101 * - views-view-fields--page.html.twig
1102 * - views-view-fields--foobar.html.twig
1103 * - views-view-fields.html.twig
1105 * Important! When adding a new template to your theme, be sure to flush the
1106 * theme registry cache!
1108 * @ingroup views_overview
1109 * @see \Drupal\views\ViewExecutable::buildThemeFunctions()