From 2250c09addf51b6c042ccc46fb9fc92ac5dfdff9 Mon Sep 17 00:00:00 2001 From: webchick <webchick@24967.no-reply.drupal.org> Date: Sun, 4 May 2014 00:07:44 -0700 Subject: [PATCH] Issue #1939008 by sun, joelpittet, gnuget, Gokul N K, sphism, drupalninja99, c4rl, Cottser, mdrummond, long wave, steveoliver, andypost, Fabianx | jenlampton: Convert theme_table() to Twig. --- core/includes/theme.inc | 164 ++++++++---------- .../Tests/NodeTypeRenameConfigImportTest.php | 32 +++- .../Drupal/system/Tests/Theme/TableTest.php | 52 ++++-- core/modules/system/templates/table.html.twig | 81 +++++++++ 4 files changed, 220 insertions(+), 109 deletions(-) create mode 100644 core/modules/system/templates/table.html.twig diff --git a/core/includes/theme.inc b/core/includes/theme.inc index b62ad74d6ce5..654803cf3b3e 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -1411,9 +1411,11 @@ function drupal_pre_render_table(array $element) { } /** - * Returns HTML for a table. + * Prepares variables for table templates. * - * @param $variables + * Default template: table.html.twig. + * + * @param array $variables * An associative array containing: * - header: An array containing the table headers. Each element of the array * can be either a localized string or an associative array with the @@ -1495,83 +1497,66 @@ function drupal_pre_render_table(array $element) { * - empty: The message to display in an extra row if table does not have any * rows. */ -function theme_table($variables) { - $header = $variables['header']; - $rows = $variables['rows']; - $attributes = $variables['attributes']; - $caption = $variables['caption']; - $colgroups = $variables['colgroups']; - $sticky = $variables['sticky']; - $responsive = $variables['responsive']; - $empty = $variables['empty']; - - $output = '<table' . new Attribute($attributes) . ">\n"; - - if (isset($caption)) { - $output .= '<caption>' . $caption . "</caption>\n"; - } +function template_preprocess_table(&$variables) { + $is_sticky = !empty($variables['sticky']); + $is_responsive = !empty($variables['responsive']); // Format the table columns: - if (count($colgroups)) { - foreach ($colgroups as $colgroup) { - $attributes = array(); - + if (!empty($variables['colgroups'])) { + foreach ($variables['colgroups'] as &$colgroup) { // Check if we're dealing with a simple or complex column if (isset($colgroup['data'])) { - foreach ($colgroup as $key => $value) { - if ($key == 'data') { - $cols = $value; - } - else { - $attributes[$key] = $value; - } - } + $cols = $colgroup['data']; + unset($colgroup['data']); + $colgroup_attributes = $colgroup; } else { $cols = $colgroup; + $colgroup_attributes = array(); } - - // Build colgroup - if (is_array($cols) && count($cols)) { - $output .= ' <colgroup' . new Attribute($attributes) . '>'; - foreach ($cols as $col) { - $output .= ' <col' . new Attribute($col) . ' />'; + $colgroup = array(); + $colgroup['attributes'] = new Attribute($colgroup_attributes); + $colgroup['cols'] = array(); + + // Build columns. + if (is_array($cols) && !empty($cols)) { + foreach ($cols as $col_key => $col) { + $colgroup['cols'][$col_key]['attributes'] = new Attribute($col); } - $output .= " </colgroup>\n"; - } - else { - $output .= ' <colgroup' . new Attribute($attributes) . " />\n"; } } } // Add the 'empty' row message if available. - if (!count($rows) && $empty) { + if (empty($variables['rows']) && isset($variables['empty'])) { $header_count = 0; - foreach ($header as $header_cell) { - if (is_array($header_cell)) { - $header_count += isset($header_cell['colspan']) ? $header_cell['colspan'] : 1; + foreach ($variables['header'] as $header_cell) { + if (is_array($header_cell) && isset($header_cell['colspan'])) { + $header_count += $header_cell['colspan']; } else { $header_count++; } } - $rows[] = array(array('data' => $empty, 'colspan' => $header_count, 'class' => array('empty', 'message'))); + $variables['rows'][] = array(array( + 'data' => $variables['empty'], + 'colspan' => $header_count, + 'class' => array('empty', 'message'), + )); } - $responsive = array(); + // Build an associative array of responsive classes keyed by column. + $responsive_classes = array(); + // Format the table header: - if (count($header)) { - $ts = tablesort_init($header); - // HTML requires that the thead tag has tr tags in it followed by tbody - // tags. Using ternary operator to check and see if we have any rows. - $output .= (count($rows) ? ' <thead><tr>' : ' <tr>'); - $i = 0; - foreach ($header as $cell) { - $i++; + $ts = array(); + if (!empty($variables['header'])) { + $ts = tablesort_init($variables['header']); + + foreach ($variables['header'] as $col_key => $cell) { if (!is_array($cell)) { $cell_content = $cell; - $cell_attributes = ''; + $cell_attributes = new Attribute(); $is_header = TRUE; } else { @@ -1590,10 +1575,10 @@ function theme_table($variables) { // must be transferred to the content cells. if (!empty($cell['class']) && is_array($cell['class'])) { if (in_array(RESPONSIVE_PRIORITY_MEDIUM, $cell['class'])) { - $responsive[$i] = RESPONSIVE_PRIORITY_MEDIUM; + $responsive_classes[$col_key] = RESPONSIVE_PRIORITY_MEDIUM; } elseif (in_array(RESPONSIVE_PRIORITY_LOW, $cell['class'])) { - $responsive[$i] = RESPONSIVE_PRIORITY_LOW; + $responsive_classes[$col_key] = RESPONSIVE_PRIORITY_LOW; } } @@ -1601,54 +1586,50 @@ function theme_table($variables) { $cell_content = drupal_render($cell_content); } - tablesort_header($cell_content, $cell, $header, $ts); + tablesort_header($cell_content, $cell, $variables['header'], $ts); // tablesort_header() removes the 'sort' and 'field' keys. $cell_attributes = new Attribute($cell); } - $cell_tag = $is_header ? 'th' : 'td'; - $output .= '<' . $cell_tag . $cell_attributes . '>' . $cell_content . '</' . $cell_tag . '>'; + $variables['header'][$col_key] = array(); + $variables['header'][$col_key]['tag'] = $is_header ? 'th' : 'td'; + $variables['header'][$col_key]['attributes'] = $cell_attributes; + $variables['header'][$col_key]['content'] = $cell_content; } - // Using ternary operator to close the tags based on whether or not there are rows - $output .= (count($rows) ? " </tr></thead>\n" : "</tr>\n"); - } - else { - $ts = array(); } - // Format the table rows: - if (count($rows)) { - $output .= "<tbody>\n"; + if (!empty($variables['rows'])) { $flip = array('even' => 'odd', 'odd' => 'even'); $class = 'even'; - foreach ($rows as $row) { + foreach ($variables['rows'] as $row_key => $row) { // Check if we're dealing with a simple or complex row if (isset($row['data'])) { $cells = $row['data']; $no_striping = isset($row['no_striping']) ? $row['no_striping'] : FALSE; // Set the attributes array and exclude 'data' and 'no_striping'. - $attributes = $row; - unset($attributes['data']); - unset($attributes['no_striping']); + $row_attributes = $row; + unset($row_attributes['data']); + unset($row_attributes['no_striping']); } else { $cells = $row; - $attributes = array(); + $row_attributes = array(); $no_striping = FALSE; } - if (count($cells)) { - // Add odd/even class - if (!$no_striping) { - $class = $flip[$class]; - $attributes['class'][] = $class; - } - // Build row - $output .= ' <tr' . new Attribute($attributes) . '>'; - $i = 0; - foreach ($cells as $cell) { - $i++; + // Add odd/even class. + if (!$no_striping) { + $class = $flip[$class]; + $row_attributes['class'][] = $class; + } + + // Build row. + $variables['rows'][$row_key] = array(); + $variables['rows'][$row_key]['attributes'] = new Attribute($row_attributes); + $variables['rows'][$row_key]['cells'] = array(); + if (!empty($cells)) { + foreach ($cells as $col_key => $cell) { if (!is_array($cell)) { $cell_content = $cell; $cell_attributes = array(); @@ -1671,26 +1652,22 @@ function theme_table($variables) { } } // Add active class if needed for sortable tables. - if (isset($header[$i]['data']) && $header[$i]['data'] == $ts['name'] && !empty($header[$i]['field'])) { + if (isset($variables['header'][$col_key]['data']) && $variables['header'][$col_key]['data'] == $ts['name'] && !empty($variables['header'][$col_key]['field'])) { $cell_attributes['class'][] = 'active'; } // Copy RESPONSIVE_PRIORITY_LOW/RESPONSIVE_PRIORITY_MEDIUM // class from header to cell as needed. - if (isset($responsive[$i])) { - $cell_attributes['class'][] = $responsive[$i]; + if (isset($responsive_classes[$col_key])) { + $cell_attributes['class'][] = $responsive_classes[$col_key]; } - $cell_tag = $is_header ? 'th' : 'td'; - $output .= '<' . $cell_tag . new Attribute($cell_attributes) . '>' . $cell_content . '</' . $cell_tag . '>'; + $variables['rows'][$row_key]['cells'][$col_key]['tag'] = $is_header ? 'th' : 'td'; + $variables['rows'][$row_key]['cells'][$col_key]['attributes'] = new Attribute($cell_attributes); + $variables['rows'][$row_key]['cells'][$col_key]['content'] = $cell_content; } - $output .= " </tr>\n"; } } - $output .= "</tbody>\n"; } - - $output .= "</table>\n"; - return $output; } /** @@ -2577,6 +2554,7 @@ function drupal_common_theme() { ), 'table' => array( 'variables' => array('header' => NULL, 'rows' => NULL, 'attributes' => array(), 'caption' => NULL, 'colgroups' => array(), 'sticky' => FALSE, 'responsive' => TRUE, 'empty' => ''), + 'template' => 'table', ), 'tablesort_indicator' => array( 'variables' => array('style' => NULL), diff --git a/core/modules/node/lib/Drupal/node/Tests/NodeTypeRenameConfigImportTest.php b/core/modules/node/lib/Drupal/node/Tests/NodeTypeRenameConfigImportTest.php index bd2ba66d09c7..5a189abb464c 100644 --- a/core/modules/node/lib/Drupal/node/Tests/NodeTypeRenameConfigImportTest.php +++ b/core/modules/node/lib/Drupal/node/Tests/NodeTypeRenameConfigImportTest.php @@ -9,6 +9,7 @@ use Drupal\Component\Utility\String; use Drupal\Component\Utility\Unicode; +use Drupal\Component\Utility\Xss; use Drupal\Core\Config\Entity\ConfigEntityStorage; use Drupal\simpletest\WebTestBase; @@ -106,8 +107,12 @@ public function testConfigurationRename() { $entity_type = \Drupal::entityManager()->getDefinition($config_entity_type); $old_id = ConfigEntityStorage::getIDFromConfigName($names['old_name'], $entity_type->getConfigPrefix()); $new_id = ConfigEntityStorage::getIDFromConfigName($names['new_name'], $entity_type->getConfigPrefix()); - $this->assertText('-' . $entity_type->getKey('id') . ': ' . $old_id); - $this->assertText('+' . $entity_type->getKey('id') . ': ' . $new_id); + + $id_key = $entity_type->getKey('id'); + $text = "$id_key: $old_id"; + $this->assertTextPattern('/\-\s+' . preg_quote($text, '/') . '/', "'-$text' found."); + $text = "$id_key: $new_id"; + $this->assertTextPattern('/\+\s+' . preg_quote($text, '/') . '/', "'+$text' found."); } // Run the import. @@ -119,4 +124,27 @@ public function testConfigurationRename() { $this->assertIdentical($staged_type, $content_type->type); } + /** + * Asserts that a Perl regex pattern is found in the text content. + * + * @param string $pattern + * Perl regex to look for including the regex delimiters. + * @param string $message + * (optional) A message to display with the assertion. + * + * @return bool + * TRUE on pass, FALSE on failure. + */ + protected function assertTextPattern($pattern, $message = NULL) { + // @see WebTestBase::assertTextHelper() + if ($this->plainTextContent === FALSE) { + $this->plainTextContent = Xss::filter($this->drupalGetContent(), array()); + } + // @see WebTestBase::assertPattern() + if (!$message) { + $message = String::format('Pattern "@pattern" found', array('@pattern' => $pattern)); + } + return $this->assert((bool) preg_match($pattern, $this->plainTextContent), $message); + } + } diff --git a/core/modules/system/lib/Drupal/system/Tests/Theme/TableTest.php b/core/modules/system/lib/Drupal/system/Tests/Theme/TableTest.php index 26a0fd8292b2..6b9c5ef95cc6 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Theme/TableTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Theme/TableTest.php @@ -7,6 +7,7 @@ namespace Drupal\system\Tests\Theme; +use Drupal\Component\Utility\String; use Drupal\simpletest\DrupalUnitTestBase; /** @@ -41,10 +42,10 @@ function testThemeTableStickyHeaders() { '#rows' => $rows, '#sticky' => TRUE, ); - $this->content = drupal_render($table); + $this->render($table); $js = _drupal_add_js(); - $this->assertTrue(isset($js['core/misc/tableheader.js']), 'tableheader.js was included when $sticky = TRUE.'); - $this->assertRaw('sticky-enabled', 'Table has a class of sticky-enabled when $sticky = TRUE.'); + $this->assertTrue(isset($js['core/misc/tableheader.js']), 'tableheader.js found.'); + $this->assertRaw('sticky-enabled'); drupal_static_reset('_drupal_add_js'); } @@ -66,10 +67,10 @@ function testThemeTableNoStickyHeaders() { '#colgroups' => $colgroups, '#sticky' => FALSE, ); - $this->content = drupal_render($table); + $this->render($table); $js = _drupal_add_js(); - $this->assertFalse(isset($js['core/misc/tableheader.js']), 'tableheader.js was not included because $sticky = FALSE.'); - $this->assertNoRaw('sticky-enabled', 'Table does not have a class of sticky-enabled because $sticky = FALSE.'); + $this->assertFalse(isset($js['core/misc/tableheader.js']), 'tableheader.js not found.'); + $this->assertNoRaw('sticky-enabled'); drupal_static_reset('_drupal_add_js'); } @@ -79,9 +80,9 @@ function testThemeTableNoStickyHeaders() { */ function testThemeTableWithEmptyMessage() { $header = array( - t('Header 1'), + 'Header 1', array( - 'data' => t('Header 2'), + 'data' => 'Header 2', 'colspan' => 2, ), ); @@ -89,11 +90,12 @@ function testThemeTableWithEmptyMessage() { '#type' => 'table', '#header' => $header, '#rows' => array(), - '#empty' => t('No strings available.'), + '#empty' => 'Empty row.', ); - $this->content = drupal_render($table); - $this->assertRaw('<tr class="odd"><td colspan="3" class="empty message">No strings available.</td>', 'Correct colspan was set on empty message.'); - $this->assertRaw('<thead><tr><th>Header 1</th>', 'Table header was printed.'); + $this->render($table); + $this->removeWhiteSpace(); + $this->assertRaw('<thead><tr><th>Header 1</th><th colspan="2">Header 2</th></tr>', 'Table header found.'); + $this->assertRaw('<tr class="odd"><td colspan="3" class="empty message">Empty row.</td>', 'Colspan on #empty row found.'); } /** @@ -110,7 +112,7 @@ function testThemeTableWithNoStriping() { '#type' => 'table', '#rows' => $rows, ); - $this->content = drupal_render($table); + $this->render($table); $this->assertNoRaw('class="odd"', 'Odd/even classes were not added because $no_striping = TRUE.'); $this->assertNoRaw('no_striping', 'No invalid no_striping HTML attribute was printed.'); } @@ -130,10 +132,32 @@ function testThemeTableHeaderCellOption() { '#type' => 'table', '#rows' => $rows, ); - $this->content = drupal_render($table); + $this->render($table); + $this->removeWhiteSpace(); $this->assertRaw('<th>1</th><td>1</td><td>1</td>', 'The th and td tags was printed correctly.'); } + /** + * Renders a given render array. + * + * @param array $elements + * The render array elements to render. + * + * @return string + * The rendered HTML. + */ + protected function render(array $elements) { + $this->content = drupal_render($elements); + $this->verbose('<pre>' . String::checkPlain($this->content)); + } + + /** + * Removes all white-space between HTML tags from $this->content. + */ + protected function removeWhiteSpace() { + $this->content = preg_replace('@>\s+<@', '><', $this->content); + } + /** * Asserts that a raw string appears in $this->content. * diff --git a/core/modules/system/templates/table.html.twig b/core/modules/system/templates/table.html.twig new file mode 100644 index 000000000000..ce69286108b8 --- /dev/null +++ b/core/modules/system/templates/table.html.twig @@ -0,0 +1,81 @@ +{# +/** + * @file + * Default theme implementation to display a table. + * + * Available variables: + * - attributes: HTML attributes to apply to the <table> tag. + * - caption: A localized string for the <caption> tag. + * - colgroups: Column groups. Each group contains the following properties: + * - attributes: HTML attributes to apply to the <col> tag. + * Note: Drupal currently supports only one table header row, see + * http://drupal.org/node/893530 and + * http://api.drupal.org/api/drupal/includes!theme.inc/function/theme_table/7#comment-5109. + * - header: Table header cells. Each cell contains the following properties: + * - tag: The HTML tag name to use; either TH or TD. + * - attributes: HTML attributes to apply to the tag. + * - content: A localized string for the title of the column. + * - field: Field name (required for column sorting). + * - sort: Default sort order for this column ("asc" or "desc"). + * - sticky: A flag indicating whether to use a "sticky" table header. + * - rows: Table rows. Each row contains the following properties: + * - attributes: HTML attributes to apply to the <tr> tag. + * - data: Table cells. + * - no_striping: A flag indicating that the row should receive no + * 'even / odd' styling. Defaults to FALSE. + * - cells: Table cells of the row. Each cell contains the following keys: + * - tag: The HTML tag name to use; either TH or TD. + * - attributes: Any HTML attributes, such as "colspan", to apply to the + * table cell. + * - content: The string to display in the table cell. + * - empty: The message to display in an extra row if table does not have + * any rows. + * + * @see template_preprocess_table() + * + * @ingroup themeable + */ +#} +<table{{ attributes }}> + {% if caption %} + <caption>{{ caption }}</caption> + {% endif %} + + {% for colgroup in colgroups %} + {% if colgroup.cols %} + <colgroup{{ colgroup.attributes }}> + {% for col in colgroup.cols %} + <col{{ col.attributes }} /> + {% endfor %} + </colgroup> + {% else %} + <colgroup{{ colgroup.attributes }} /> + {% endif %} + {% endfor %} + + {% if header %} + <thead> + <tr> + {% for cell in header %} + <{{ cell.tag }}{{ cell.attributes }}> + {{- cell.content -}} + </{{ cell.tag }}> + {% endfor %} + </tr> + </thead> + {% endif %} + + {% if rows %} + <tbody> + {% for row in rows %} + <tr{{ row.attributes }}> + {% for cell in row.cells %} + <{{ cell.tag }}{{ cell.attributes }}> + {{- cell.content -}} + </{{ cell.tag }}> + {% endfor %} + </tr> + {% endfor %} + </tbody> + {% endif %} +</table> -- GitLab