diff --git a/core/includes/theme.inc b/core/includes/theme.inc index b62ad74..654803c 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 = '\n"; - - if (isset($caption)) { - $output .= '' . $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 .= ' '; - foreach ($cols as $col) { - $output .= ' '; + $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 .= " \n"; - } - else { - $output .= ' \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) ? ' ' : ' '); - $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 . ''; + $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) ? " \n" : "\n"); - } - else { - $ts = array(); } - // Format the table rows: - if (count($rows)) { - $output .= "\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 .= ' '; - $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 . ''; + $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 .= " \n"; } } - $output .= "\n"; } - - $output .= "\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/system/lib/Drupal/system/Tests/Theme/TableTest.php b/core/modules/system/lib/Drupal/system/Tests/Theme/TableTest.php index 26a0fd8..6b9c5ef 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('No strings available.', 'Correct colspan was set on empty message.'); - $this->assertRaw('Header 1', 'Table header was printed.'); + $this->render($table); + $this->removeWhiteSpace(); + $this->assertRaw('Header 1Header 2', 'Table header found.'); + $this->assertRaw('Empty row.', '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,11 +132,33 @@ function testThemeTableHeaderCellOption() { '#type' => 'table', '#rows' => $rows, ); - $this->content = drupal_render($table); + $this->render($table); + $this->removeWhiteSpace(); $this->assertRaw('111', '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('
' . 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.
    *
    * @param string $value
diff --git a/core/modules/system/templates/table.html.twig b/core/modules/system/templates/table.html.twig
new file mode 100644
index 0000000..ce69286
--- /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  tag.
+ * - caption: A localized string for the 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  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
+ */
+#}
+
+  {% if caption %}
+    
+  {% endif %}
+
+  {% for colgroup in colgroups %}
+    {% if colgroup.cols %}
+      
+        {% for col in colgroup.cols %}
+          
+        {% endfor %}
+      
+    {% else %}
+      
+    {% endif %}
+  {% endfor %}
+
+  {% if header %}
+    
+      
+        {% for cell in header %}
+          <{{ cell.tag }}{{ cell.attributes }}>
+            {{- cell.content -}}
+          
+        {% endfor %}
+      
+    
+  {% endif %}
+
+  {% if rows %}
+    
+      {% for row in rows %}
+        
+          {% for cell in row.cells %}
+            <{{ cell.tag }}{{ cell.attributes }}>
+              {{- cell.content -}}
+            
+          {% endfor %}
+        
+      {% endfor %}
+    
+  {% endif %}
+
tag. + * - colgroups: Column groups. Each group contains the following properties: + * - attributes: HTML attributes to apply to the
{{ caption }}