diff --git a/core/modules/views/config/schema/views.style.schema.yml b/core/modules/views/config/schema/views.style.schema.yml index 49ab0f826f12c7da9fe1909bb338a5a008f5b984..dc6edd0b225b7196406a9d8926f5ed48ecbea962 100644 --- a/core/modules/views/config/schema/views.style.schema.yml +++ b/core/modules/views/config/schema/views.style.schema.yml @@ -29,15 +29,24 @@ views.style.grid: columns: type: integer label: 'Number of columns' + automatic_width: + type: boolean + label: 'Automatic width' alignment: type: string label: 'Alignment' - fill_single_line: + row_class_custom: + type: string + label: 'Custom row classes' + row_class_default: type: boolean - label: 'Fill up single line' - summary: + label: 'Default views row classes' + col_class_custom: type: string - label: 'Table summary' + label: 'Custom column classes' + col_class_default: + type: boolean + label: 'Default views column classes' views.style.table: type: views_style diff --git a/core/modules/views/css/views.module.css b/core/modules/views/css/views.module.css index 7ca2f497fd2775d64b6beee764431299ee4ff4e1..e422122cb4e4ba13a99ddc0cfe51b344a945bb13 100644 --- a/core/modules/views/css/views.module.css +++ b/core/modules/views/css/views.module.css @@ -22,3 +22,13 @@ .view .progress-disabled { float: none; } + +/* Grid style column align. */ +.views-view-grid .views-col { + float: left; +} +.views-view-grid .views-row { + clear: both; + float: left; + width: 100%; +} diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/style/Grid.php b/core/modules/views/lib/Drupal/views/Plugin/views/style/Grid.php index 06316616140b09f9107c1cdb753fb7764b75ae26..fb4c05deba20d0387c8ef55920055da44e0e3aeb 100644 --- a/core/modules/views/lib/Drupal/views/Plugin/views/style/Grid.php +++ b/core/modules/views/lib/Drupal/views/Plugin/views/style/Grid.php @@ -33,28 +33,22 @@ class Grid extends StylePluginBase { protected $usesRowPlugin = TRUE; /** - * Does the style plugin support custom css class for the rows. - * - * @var bool - */ - protected $usesRowClass = TRUE; - - /** - * Set default options + * {@inheritdoc} */ protected function defineOptions() { $options = parent::defineOptions(); - $options['columns'] = array('default' => '4'); + $options['automatic_width'] = array('default' => TRUE); $options['alignment'] = array('default' => 'horizontal'); - $options['fill_single_line'] = array('default' => TRUE, 'bool' => TRUE); - $options['summary'] = array('default' => ''); - + $options['col_class_custom'] = array('default' => ''); + $options['col_class_default'] = array('default' => TRUE); + $options['row_class_custom'] = array('default' => ''); + $options['row_class_default'] = array('default' => TRUE); return $options; } /** - * Render the given style. + * {@inheritdoc} */ public function buildOptionsForm(&$form, &$form_state) { parent::buildOptionsForm($form, $form_state); @@ -63,7 +57,13 @@ public function buildOptionsForm(&$form, &$form_state) { '#title' => t('Number of columns'), '#default_value' => $this->options['columns'], '#required' => TRUE, - '#min' => 0, + '#min' => 1, + ); + $form['automatic_width'] = array( + '#type' => 'checkbox', + '#title' => t('Automatic width'), + '#description' => t('The width of each column will be calculated automatically based on the number of columns provided. If additional classes are entered or a theme injects classes based on a grid system, disabling this option may prove beneficial.'), + '#default_value' => $this->options['automatic_width'], ); $form['alignment'] = array( '#type' => 'radios', @@ -72,20 +72,36 @@ public function buildOptionsForm(&$form, &$form_state) { '#default_value' => $this->options['alignment'], '#description' => t('Horizontal alignment will place items starting in the upper left and moving right. Vertical alignment will place items starting in the upper left and moving down.'), ); - - $form['fill_single_line'] = array( + $form['col_class_default'] = array( + '#title' => t('Default column classes'), + '#description' => t('Add the default views column classes like views-col, col-1 and clearfix 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.'), '#type' => 'checkbox', - '#title' => t('Fill up single line'), - '#description' => t('If you disable this option, a grid with only one row will have the same number of table cells (<TD>) as items. Disabling it can cause problems with your CSS.'), - '#default_value' => !empty($this->options['fill_single_line']), + '#default_value' => $this->options['col_class_default'], ); - - $form['summary'] = array( + $form['col_class_custom'] = array( + '#title' => t('Custom column class'), + '#description' => t('Additional classes to provide on each column. Separated by a space.'), + '#type' => 'textfield', + '#default_value' => $this->options['col_class_custom'], + ); + if ($this->usesFields()) { + $form['col_class_custom']['#description'] .= ' ' . t('You may use field tokens from as per the "Replacement patterns" used in "Rewrite the output of this field" for all fields.'); + } + $form['row_class_default'] = array( + '#title' => t('Default row classes'), + '#description' => t('Adds the default views row classes like views-row, row-1 and clearfix 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.'), + '#type' => 'checkbox', + '#default_value' => $this->options['row_class_default'], + ); + $form['row_class_custom'] = array( + '#title' => t('Custom row class'), + '#description' => t('Additional classes to provide on each row. Separated by a space.'), '#type' => 'textfield', - '#title' => t('Table summary'), - '#description' => t('This value will be displayed as table-summary attribute in the html. Set this for better accessiblity of your site.'), - '#default_value' => $this->options['summary'], + '#default_value' => $this->options['row_class_custom'], ); + if ($this->usesFields()) { + $form['row_class_custom']['#description'] .= ' ' . t('You may use field tokens from as per the "Replacement patterns" used in "Rewrite the output of this field" for all fields.'); + } } } diff --git a/core/modules/views/lib/Drupal/views/Tests/Plugin/StyleGridTest.php b/core/modules/views/lib/Drupal/views/Tests/Plugin/StyleGridTest.php new file mode 100644 index 0000000000000000000000000000000000000000..da2b95ca71afb560bdf2dc647d4a9caad293d327 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/Plugin/StyleGridTest.php @@ -0,0 +1,107 @@ +<?php + +/** + * @file + * Contains \Drupal\views\Tests\Plugin\StyleGridTest. + */ + +namespace Drupal\views\Tests\Plugin; + +use Drupal\views\ViewExecutable; +use Symfony\Component\HttpFoundation\Request; + +/** + * Tests the grid style plugin. + * + * @see \Drupal\views\Plugin\views\style\Grid + */ +class StyleGridTest extends PluginTestBase { + + /** + * Views used by this test. + * + * @var array + */ + public static $testViews = array('test_grid'); + + /** + * Keeps track of which alignments have been tested. + */ + protected $alignmentsTested = array(); + + /** + * {@inheritdoc} + */ + public static function getInfo() { + return array( + 'name' => 'Style: Grid', + 'description' => 'Tests the grid style plugin.', + 'group' => 'Views Plugins', + ); + } + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + $this->enableViewsTestModule(); + } + + /** + * Tests the grid style. + */ + public function testGrid() { + $view = views_get_view('test_grid'); + foreach (array('horizontal', 'vertical') as $alignment) { + $this->assertGrid($view, $alignment, 5); + $this->assertGrid($view, $alignment, 4); + $this->assertGrid($view, $alignment, 3); + $this->assertGrid($view, $alignment, 2); + $this->assertGrid($view, $alignment, 1); + } + } + + /** + * Generates a grid and asserts that it is displaying correctly. + * + * @param \Drupal\views\ViewExecutable $view + * The executable to prepare. + * @param string $alignment + * The alignment of the grid to test. + * @param int $columns + * The number of columns in the grid to test. + */ + protected function assertGrid(ViewExecutable $view, $alignment, $columns) { + $view->setDisplay('default'); + $view->initStyle(); + $view->initHandlers(); + $view->initQuery(); + $view->style_plugin->options['alignment'] = $alignment; + $view->style_plugin->options['columns'] = $columns; + $this->executeView($view); + $output = $view->preview(); + $output = drupal_render($output); + $this->drupalSetContent($output, 'internal://test-grid'); + if (!in_array($alignment, $this->alignmentsTested)) { + $result = $this->xpath('//div[contains(@class, "views-view-grid") and contains(@class, :alignment) and contains(@class, :columns)]', array(':alignment' => $alignment, ':columns' => 'cols-' . $columns)); + $this->assertTrue(count($result), ucfirst($alignment) . " grid markup detected."); + $this->alignmentsTested[] = $alignment; + } + $width = '0'; + switch ($columns) { + case 5: $width = '20'; break; + case 4: $width = '25'; break; + case 3: $width = '33.3333'; break; + case 2: $width = '50'; break; + case 1: $width = '100'; break; + } + // Ensure last column exists. + $result = $this->xpath('//div[contains(@class, "views-col") and contains(@class, :columns) and starts-with(@style, :width)]', array(':columns' => 'col-' . $columns, ':width' => 'width: ' . $width)); + $this->assertTrue(count($result), ucfirst($alignment) . " $columns column grid: last column exists and automatic width calculated correctly."); + // Ensure no extra columns were generated. + $result = $this->xpath('//div[contains(@class, "views-col") and contains(@class, :columns)]', array(':columns' => 'col-' . ($columns + 1))); + $this->assertFalse(count($result), ucfirst($alignment) . " $columns column grid: no extraneous columns exist."); + } + +} diff --git a/core/modules/views/templates/views-view-grid.html.twig b/core/modules/views/templates/views-view-grid.html.twig index 8b63792f825712fa63a9ca5dfc61f2ad6c4df921..a5a813cce35c0db55973653b15f7cdb78d9f15fd 100644 --- a/core/modules/views/templates/views-view-grid.html.twig +++ b/core/modules/views/templates/views-view-grid.html.twig @@ -4,13 +4,18 @@ * Default theme implementation for views to display rows in a grid. * * Available variables: - * - attributes: HTML attributes for the table element. + * - attributes: HTML attributes for the wrapping element. * - title: The title of this group of rows. - * - rows: A list of rows. Each row contains a list of columns. - * - row_classes: HTML classes for each row including the row number and first - * or last. - * - column_classes: HTML classes for each column including the row number and - * first or last. + * - view: The view object. + * - rows: The rendered view results. + * - options: The view plugin style options. + * - items: A list of grid items. Each item contains a list of rows or columns. + * The order in what comes first (row or column) depends on which alignment + * type is chosen (horizontal or vertical). + * - attributes: HTML attributes for each row or column. + * - content: A list of columns or rows. Each row or column contains: + * - attributes: HTML attributes for each row or column. + * - content: The row or column contents. * * @see template_preprocess_views_view_grid() * @@ -20,16 +25,26 @@ {% if title %} <h3>{{ title }}</h3> {% endif %} -<table{{ attributes }}> - <tbody> - {% for row_number, columns in rows %} - <tr{{ row_classes[row_number] }}> - {% for column_number, item in columns %} - <td{{ column_classes[row_number][column_number] }}> - {{ item }} - </td> - {% endfor %} - </tr> +<div{{ attributes }}> +{% if options.alignment == 'horizontal' %} + {% for row in items %} + <div{{ row.attributes }}> + {% for column in row.content %} + <div{{ column.attributes }}> + {{ column.content }} + </div> {% endfor %} - </tbody> -</table> + </div> + {% endfor %} +{% else %} + {% for column in items %} + <div{{ column.attributes }}> + {% for row in column.content %} + <div{{ row.attributes }}> + {{ row.content }} + </div> + {% endfor %} + </div> + {% endfor %} +{% endif %} +</div> diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_grid.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_grid.yml new file mode 100644 index 0000000000000000000000000000000000000000..c2a8915cb03901a7af2b296c7772632af76dd9d4 --- /dev/null +++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_grid.yml @@ -0,0 +1,71 @@ +base_table: views_test_data +core: '8' +description: '' +status: '1' +display: + default: + display_options: + defaults: + fields: '0' + pager: '0' + pager_options: '0' + sorts: '0' + fields: + age: + field: age + id: age + relationship: none + table: views_test_data + plugin_id: numeric + id: + field: id + id: id + relationship: none + table: views_test_data + plugin_id: numeric + name: + field: name + id: name + relationship: none + table: views_test_data + plugin_id: string + pager: + options: + offset: '0' + type: none + pager_options: { } + sorts: + id: + field: id + id: id + order: ASC + relationship: none + table: views_test_data + plugin_id: numeric + style: + type: grid + options: + grouping: { } + columns: '4' + automatic_width: '1' + alignment: horizontal + col_class_default: '1' + col_class_custom: '' + row_class_default: '1' + row_class_custom: '' + row: + type: fields + display_plugin: default + display_title: Master + id: default + position: '0' + page_1: + display_options: + path: test-grid + display_plugin: page + display_title: 'Page display' + id: page_1 + position: '1' +label: '' +id: test_grid +tag: '' diff --git a/core/modules/views/views.theme.inc b/core/modules/views/views.theme.inc index 3d1a84dc0e2e985b2970418ddd60db95e4054b6e..5ee51435aa877a1cbf80d65de6c92d5a20992660 100644 --- a/core/modules/views/views.theme.inc +++ b/core/modules/views/views.theme.inc @@ -746,56 +746,102 @@ function template_preprocess_views_view_table(&$variables) { * - rows: An array of row items. Each row is an array of content. */ function template_preprocess_views_view_grid(&$variables) { - $view = $variables['view']; - $options = $view->style_plugin->options; - $handler = $view->style_plugin; - $default_row_class = isset($options['default_row_class']) ? $options['default_row_class'] : TRUE; - $row_class_special = isset($options['row_class_special']) ? $options['row_class_special'] : TRUE; + $options = $variables['options'] = $variables['view']->style_plugin->options; + $horizontal = ($options['alignment'] === 'horizontal'); + + $variables['attributes']['class'] = array( + 'views-view-grid', + $options['alignment'], + 'cols-' . $options['columns'], + 'clearfix', + ); + + $col = 0; + $row = 0; + $items = array(); + $remainders = count($variables['rows']) % $options['columns']; + $num_rows = floor(count($variables['rows']) / $options['columns']); - $columns = $options['columns']; - $variables['attributes']['class'][] = 'views-view-grid cols-' . $columns; - - $rows = array(); - $row_indexes = array(); - - if ($options['alignment'] == 'horizontal') { - $row = array(); - $col_count = 0; - $row_count = 0; - $count = 0; - foreach ($variables['rows'] as $row_index => $item) { - $count++; - $row[] = $item; - $row_indexes[$row_count][$col_count] = $row_index; - $col_count++; - if ($count % $columns == 0) { - $rows[] = $row; - $row = array(); - $col_count = 0; - $row_count++; + // Iterate over each rendered views result row. + foreach ($variables['rows'] as $item) { + + // Add the item. + if ($horizontal) { + $items[$row]['content'][$col]['content'] = $item; + } + else { + $items[$col]['content'][$row]['content'] = $item; + } + + // Create attributes for rows. + if (!$horizontal || ($horizontal && empty($items[$row]['attributes']))) { + $row_attributes = array('class' => array()); + // Add default views row classes. + if ($options['row_class_default']) { + $row_attributes['class'][] = 'views-row'; + $row_attributes['class'][] = 'row-' . ($row + 1); + if ($horizontal) { + $row_attributes['class'][] = 'clearfix'; + } + } + // Add custom row classes. + $row_class = array_filter(explode(' ', $options['row_class_custom'])); + if (!empty($row_class)) { + $row_attributes['class'] = array_merge($row_attributes['class'], $row_class); + } + // Add row attributes to the item. + if ($horizontal) { + $items[$row]['attributes'] = new Attribute($row_attributes); + } + else { + $items[$col]['content'][$row]['attributes'] = new Attribute($row_attributes); } } - if ($row) { - // Fill up the last line only if it's configured, but this is default. - if (!empty($handler->options['fill_single_line']) && count($rows)) { - for ($i = 0; $i < ($columns - $col_count); $i++) { - $row[] = ''; + + // Create attributes for columns. + if ($horizontal || (!$horizontal && empty($items[$col]['attributes']))) { + $col_attributes = array('class' => array()); + // Add default views column classes. + if ($options['col_class_default']) { + $col_attributes['class'][] = 'views-col'; + $col_attributes['class'][] = 'col-' . ($col + 1); + if (!$horizontal) { + $col_attributes['class'][] = 'clearfix'; } } - $rows[] = $row; + // Add custom column classes. + $col_class = array_filter(explode(' ', $options['col_class_custom'])); + if (!empty($col_class)) { + $col_attributes['class'] = array_merge($col_attributes['class'], $col_class); + } + // Add automatic width for columns. + if ($options['automatic_width']) { + $col_attributes['style'] = 'width: ' . (100 / $options['columns']) . '%;'; + } + // Add column attributes to the item. + if ($horizontal) { + $items[$row]['content'][$col]['attributes'] = new Attribute($col_attributes); + } + else { + $items[$col]['attributes'] = new Attribute($col_attributes); + } } - } - else { - $num_rows = floor(count($variables['rows']) / $columns); - // The remainders are the 'odd' columns that are slightly longer. - $remainders = count($variables['rows']) % $columns; - $row = 0; - $col = 0; - foreach ($variables['rows'] as $count => $item) { - $rows[$row][$col] = $item; - $row_indexes[$row][$col] = $count; - $row++; + // Increase, decrease or reset appropriate integers. + if ($horizontal) { + if ($col == 0 && $col != ($options['columns'] - 1)) { + $col++; + } + elseif ($col >= ($options['columns'] - 1)) { + $col = 0; + $row++; + } + else { + $col++; + } + } + else { + $row++; if (!$remainders && $row == $num_rows) { $row = 0; $col++; @@ -806,53 +852,10 @@ function template_preprocess_views_view_grid(&$variables) { $remainders--; } } - for ($i = 0; $i < count($rows[0]); $i++) { - // This should be a string so this is ok. - if (!isset($rows[count($rows) - 1][$i])) { - $rows[count($rows) - 1][$i] = ''; - } - } } - // Apply the row classes. - foreach ($rows as $row_number => $row) { - $row_classes = array(); - if ($default_row_class) { - $row_classes['class'][] = 'row-' . ($row_number + 1); - } - if ($row_class_special) { - if ($row_number == 0) { - $row_classes['class'][] = 'row-first'; - } - if (count($rows) == ($row_number + 1)) { - $row_classes['class'][] = 'row-last'; - } - } - $row_classes = new Attribute($row_classes); - - foreach ($rows[$row_number] as $column_number => $item) { - $variables['column_classes'][$row_number][$column_number] = array(); - if ($default_row_class) { - $variables['column_classes'][$row_number][$column_number]['class'][] = 'col-' . ($column_number + 1); - } - if ($row_class_special) { - if ($column_number == 0) { - $variables['column_classes'][$row_number][$column_number]['class'][] = 'col-first'; - } - elseif (count($rows[$row_number]) == ($column_number + 1)) { - $variables['column_classes'][$row_number][$column_number]['class'][] = 'col-last'; - } - } - if (isset($row_indexes[$row_number][$column_number]) && $column_class = $view->style_plugin->getRowClass($row_indexes[$row_number][$column_number])) { - $variables['column_classes'][$row_number][$column_number]['class'][] = $column_class; - } - $variables['column_classes'][$row_number][$column_number] = new Attribute($variables['column_classes'][$row_number][$column_number]); - } - } - $variables['rows'] = $rows; - if (!empty($handler->options['summary'])) { - $variables['attributes']['summary'] = $handler->options['summary']; - } + // Add items to the variables array. + $variables['items'] = $items; } /**