Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
100.00% |
1 / 1 |
|
100.00% |
0 / 0 |
CRAP | |
100.00% |
0 / 0 |
Table | |
100.00% |
1 / 1 |
|
100.00% |
5 / 5 |
47 | |
100.00% |
0 / 0 |
getInfo | |
100.00% |
1 / 1 |
1 | |
100.00% |
0 / 0 |
|||
valueCallback | |
100.00% |
1 / 1 |
5 | |
100.00% |
0 / 0 |
|||
processTable | |
100.00% |
1 / 1 |
22 | |
100.00% |
0 / 0 |
|||
validateTable | |
100.00% |
1 / 1 |
7 | |
100.00% |
0 / 0 |
|||
preRenderTable | |
100.00% |
1 / 1 |
12 | |
100.00% |
0 / 0 |
<?php | |
/** | |
* @file | |
* Contains \Drupal\Core\Render\Element\Table. | |
*/ | |
namespace Drupal\Core\Render\Element; | |
use Drupal\Core\Form\FormStateInterface; | |
use Drupal\Core\Render\Element; | |
use Drupal\Component\Utility\Html as HtmlUtility; | |
/** | |
* Provides a render element for a table. | |
* | |
* Note: Although this extends FormElement, it can be used outside the | |
* context of a form. | |
* | |
* Properties: | |
* - #header: An array of table header labels. | |
* - #rows: An array of the rows to be displayed. Each row is either an array | |
* of cell contents or an array of properties as described in table.html.twig | |
* Alternatively specify the data for the table as child elements of the table | |
* element. Table elements would contain rows elements that would in turn | |
* contain column elements. | |
* - #empty: Text to display when no rows are present. | |
* - #responsive: Indicates whether to add the drupal.responsive_table library | |
* providing responsive tables. Defaults to TRUE. | |
* - #sticky: Indicates whether to add the drupal.tableheader library that makes | |
* table headers always visible at the top of the page. Defaults to FALSE. | |
* | |
* Usage example: | |
* @code | |
* $form['contacts'] = array( | |
* '#type' => 'table', | |
* '#caption' => 'Sample Table', | |
* '#header' => array('Name', 'Phone'), | |
* ); | |
* | |
* for ($i=1; $i<=4; $i++) { | |
* $form['contacts'][$i]['name'] = array( | |
* '#type' => 'textfield', | |
* '#title' => t('Name'), | |
* '#title_display' => 'invisible', | |
* ); | |
* | |
* $form['contacts'][$i]['phone'] = array( | |
* '#type' => 'tel', | |
* '#title' => t('Phone'), | |
* '#title_display' => 'invisible', | |
* ); | |
* } | |
* @endcode | |
* @see \Drupal\Core\Render\Element\Tableselect | |
* | |
* @FormElement("table") | |
*/ | |
class Table extends FormElement { | |
/** | |
* {@inheritdoc} | |
*/ | |
public function getInfo() { | |
$class = get_class($this); | |
return array( | |
'#header' => array(), | |
'#rows' => array(), | |
'#empty' => '', | |
// Properties for tableselect support. | |
'#input' => TRUE, | |
'#tree' => TRUE, | |
'#tableselect' => FALSE, | |
'#sticky' => FALSE, | |
'#responsive' => TRUE, | |
'#multiple' => TRUE, | |
'#js_select' => TRUE, | |
'#process' => array( | |
array($class, 'processTable'), | |
), | |
'#element_validate' => array( | |
array($class, 'validateTable'), | |
), | |
// Properties for tabledrag support. | |
// The value is a list of arrays that are passed to | |
// drupal_attach_tabledrag(). Table::preRenderTable() prepends the HTML ID | |
// of the table to each set of options. | |
// @see drupal_attach_tabledrag() | |
'#tabledrag' => array(), | |
// Render properties. | |
'#pre_render' => array( | |
array($class, 'preRenderTable'), | |
), | |
'#theme' => 'table', | |
); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public static function valueCallback(&$element, $input, FormStateInterface $form_state) { | |
// If #multiple is FALSE, the regular default value of radio buttons is used. | |
if (!empty($element['#tableselect']) && !empty($element['#multiple'])) { | |
// Contrary to #type 'checkboxes', the default value of checkboxes in a | |
// table is built from the array keys (instead of array values) of the | |
// #default_value property. | |
// @todo D8: Remove this inconsistency. | |
if ($input === FALSE) { | |
$element += array('#default_value' => array()); | |
$value = array_keys(array_filter($element['#default_value'])); | |
return array_combine($value, $value); | |
} | |
else { | |
return is_array($input) ? array_combine($input, $input) : array(); | |
} | |
} | |
} | |
/** | |
* #process callback for #type 'table' to add tableselect support. | |
* | |
* @param array $element | |
* An associative array containing the properties and children of the | |
* table element. | |
* @param \Drupal\Core\Form\FormStateInterface $form_state | |
* The current state of the form. | |
* @param array $complete_form | |
* The complete form structure. | |
* | |
* @return array | |
* The processed element. | |
*/ | |
public static function processTable(&$element, FormStateInterface $form_state, &$complete_form) { | |
if ($element['#tableselect']) { | |
if ($element['#multiple']) { | |
$value = is_array($element['#value']) ? $element['#value'] : array(); | |
} | |
// Advanced selection behavior makes no sense for radios. | |
else { | |
$element['#js_select'] = FALSE; | |
} | |
// Add a "Select all" checkbox column to the header. | |
// @todo D8: Rename into #select_all? | |
if ($element['#js_select']) { | |
$element['#attached']['library'][] = 'core/drupal.tableselect'; | |
array_unshift($element['#header'], array('class' => array('select-all'))); | |
} | |
// Add an empty header column for radio buttons or when a "Select all" | |
// checkbox is not desired. | |
else { | |
array_unshift($element['#header'], ''); | |
} | |
if (!isset($element['#default_value']) || $element['#default_value'] === 0) { | |
$element['#default_value'] = array(); | |
} | |
// Create a checkbox or radio for each row in a way that the value of the | |
// tableselect element behaves as if it had been of #type checkboxes or | |
// radios. | |
foreach (Element::children($element) as $key) { | |
$row = &$element[$key]; | |
// Prepare the element #parents for the tableselect form element. | |
// Their values have to be located in child keys (#tree is ignored), | |
// since Table::validateTable() has to be able to validate whether input | |
// (for the parent #type 'table' element) has been submitted. | |
$element_parents = array_merge($element['#parents'], array($key)); | |
// Since the #parents of the tableselect form element will equal the | |
// #parents of the row element, prevent FormBuilder from auto-generating | |
// an #id for the row element, since | |
// \Drupal\Component\Utility\Html::getUniqueId() would automatically | |
// append a suffix to the tableselect form element's #id otherwise. | |
$row['#id'] = HtmlUtility::getUniqueId('edit-' . implode('-', $element_parents) . '-row'); | |
// Do not overwrite manually created children. | |
if (!isset($row['select'])) { | |
// Determine option label; either an assumed 'title' column, or the | |
// first available column containing a #title or #markup. | |
// @todo Consider to add an optional $element[$key]['#title_key'] | |
// defaulting to 'title'? | |
unset($label_element); | |
$title = NULL; | |
if (isset($row['title']['#type']) && $row['title']['#type'] == 'label') { | |
$label_element = &$row['title']; | |
} | |
else { | |
if (!empty($row['title']['#title'])) { | |
$title = $row['title']['#title']; | |
} | |
else { | |
foreach (Element::children($row) as $column) { | |
if (isset($row[$column]['#title'])) { | |
$title = $row[$column]['#title']; | |
break; | |
} | |
if (isset($row[$column]['#markup'])) { | |
$title = $row[$column]['#markup']; | |
break; | |
} | |
} | |
} | |
if (isset($title) && $title !== '') { | |
$title = t('Update @title', array('@title' => $title)); | |
} | |
} | |
// Prepend the select column to existing columns. | |
$row = array('select' => array()) + $row; | |
$row['select'] += array( | |
'#type' => $element['#multiple'] ? 'checkbox' : 'radio', | |
'#id' => HtmlUtility::getUniqueId('edit-' . implode('-', $element_parents)), | |
// @todo If rows happen to use numeric indexes instead of string keys, | |
// this results in a first row with $key === 0, which is always FALSE. | |
'#return_value' => $key, | |
'#attributes' => $element['#attributes'], | |
'#wrapper_attributes' => array( | |
'class' => array('table-select'), | |
), | |
); | |
if ($element['#multiple']) { | |
$row['select']['#default_value'] = isset($value[$key]) ? $key : NULL; | |
$row['select']['#parents'] = $element_parents; | |
} | |
else { | |
$row['select']['#default_value'] = ($element['#default_value'] == $key ? $key : NULL); | |
$row['select']['#parents'] = $element['#parents']; | |
} | |
if (isset($label_element)) { | |
$label_element['#id'] = $row['select']['#id'] . '--label'; | |
$label_element['#for'] = $row['select']['#id']; | |
$row['select']['#attributes']['aria-labelledby'] = $label_element['#id']; | |
$row['select']['#title_display'] = 'none'; | |
} | |
else { | |
$row['select']['#title'] = $title; | |
$row['select']['#title_display'] = 'invisible'; | |
} | |
} | |
} | |
} | |
return $element; | |
} | |
/** | |
* #element_validate callback for #type 'table'. | |
* | |
* @param array $element | |
* An associative array containing the properties and children of the | |
* table element. | |
* @param \Drupal\Core\Form\FormStateInterface $form_state | |
* The current state of the form. | |
* @param array $complete_form | |
* The complete form structure. | |
*/ | |
public static function validateTable(&$element, FormStateInterface $form_state, &$complete_form) { | |
// Skip this validation if the button to submit the form does not require | |
// selected table row data. | |
$triggering_element = $form_state->getTriggeringElement(); | |
if (empty($triggering_element['#tableselect'])) { | |
return; | |
} | |
if ($element['#multiple']) { | |
if (!is_array($element['#value']) || !count(array_filter($element['#value']))) { | |
$form_state->setError($element, t('No items selected.')); | |
} | |
} | |
elseif (!isset($element['#value']) || $element['#value'] === '') { | |
$form_state->setError($element, t('No item selected.')); | |
} | |
} | |
/** | |
* #pre_render callback to transform children of an element of #type 'table'. | |
* | |
* This function converts sub-elements of an element of #type 'table' to be | |
* suitable for table.html.twig: | |
* - The first level of sub-elements are table rows. Only the #attributes | |
* property is taken into account. | |
* - The second level of sub-elements is converted into columns for the | |
* corresponding first-level table row. | |
* | |
* Simple example usage: | |
* @code | |
* $form['table'] = array( | |
* '#type' => 'table', | |
* '#header' => array(t('Title'), array('data' => t('Operations'), 'colspan' => '1')), | |
* // Optionally, to add tableDrag support: | |
* '#tabledrag' => array( | |
* array( | |
* 'action' => 'order', | |
* 'relationship' => 'sibling', | |
* 'group' => 'thing-weight', | |
* ), | |
* ), | |
* ); | |
* foreach ($things as $row => $thing) { | |
* $form['table'][$row]['#weight'] = $thing['weight']; | |
* | |
* $form['table'][$row]['title'] = array( | |
* '#type' => 'textfield', | |
* '#default_value' => $thing['title'], | |
* ); | |
* | |
* // Optionally, to add tableDrag support: | |
* $form['table'][$row]['#attributes']['class'][] = 'draggable'; | |
* $form['table'][$row]['weight'] = array( | |
* '#type' => 'textfield', | |
* '#title' => t('Weight for @title', array('@title' => $thing['title'])), | |
* '#title_display' => 'invisible', | |
* '#size' => 4, | |
* '#default_value' => $thing['weight'], | |
* '#attributes' => array('class' => array('thing-weight')), | |
* ); | |
* | |
* // The amount of link columns should be identical to the 'colspan' | |
* // attribute in #header above. | |
* $form['table'][$row]['edit'] = array( | |
* '#type' => 'link', | |
* '#title' => t('Edit'), | |
* '#url' => Url::fromRoute('entity.test_entity.edit_form', ['test_entity' => $row]), | |
* ); | |
* } | |
* @endcode | |
* | |
* @param array $element | |
* A structured array containing two sub-levels of elements. Properties used: | |
* - #tabledrag: The value is a list of $options arrays that are passed to | |
* drupal_attach_tabledrag(). The HTML ID of the table is added to each | |
* $options array. | |
* | |
* @return array | |
* | |
* @see template_preprocess_table() | |
* @see \Drupal\Core\Render\AttachmentsResponseProcessorInterface::processAttachments() | |
* @see drupal_attach_tabledrag() | |
*/ | |
public static function preRenderTable($element) { | |
foreach (Element::children($element) as $first) { | |
$row = array('data' => array()); | |
// Apply attributes of first-level elements as table row attributes. | |
if (isset($element[$first]['#attributes'])) { | |
$row += $element[$first]['#attributes']; | |
} | |
// Turn second-level elements into table row columns. | |
// @todo Do not render a cell for children of #type 'value'. | |
// @see https://www.drupal.org/node/1248940 | |
foreach (Element::children($element[$first]) as $second) { | |
// Assign the element by reference, so any potential changes to the | |
// original element are taken over. | |
$column = array('data' => &$element[$first][$second]); | |
// Apply wrapper attributes of second-level elements as table cell | |
// attributes. | |
if (isset($element[$first][$second]['#wrapper_attributes'])) { | |
$column += $element[$first][$second]['#wrapper_attributes']; | |
} | |
$row['data'][] = $column; | |
} | |
$element['#rows'][] = $row; | |
} | |
// Take over $element['#id'] as HTML ID attribute, if not already set. | |
Element::setAttributes($element, array('id')); | |
// Add sticky headers, if applicable. | |
if (count($element['#header']) && $element['#sticky']) { | |
$element['#attached']['library'][] = 'core/drupal.tableheader'; | |
// Add 'sticky-enabled' class to the table to identify it for JS. | |
// This is needed to target tables constructed by this function. | |
$element['#attributes']['class'][] = 'sticky-enabled'; | |
} | |
// If the table has headers and it should react responsively to columns hidden | |
// with the classes represented by the constants RESPONSIVE_PRIORITY_MEDIUM | |
// and RESPONSIVE_PRIORITY_LOW, add the tableresponsive behaviors. | |
if (count($element['#header']) && $element['#responsive']) { | |
$element['#attached']['library'][] = 'core/drupal.tableresponsive'; | |
// Add 'responsive-enabled' class to the table to identify it for JS. | |
// This is needed to target tables constructed by this function. | |
$element['#attributes']['class'][] = 'responsive-enabled'; | |
} | |
// If the custom #tabledrag is set and there is a HTML ID, add the table's | |
// HTML ID to the options and attach the behavior. | |
if (!empty($element['#tabledrag']) && isset($element['#attributes']['id'])) { | |
foreach ($element['#tabledrag'] as $options) { | |
$options['table_id'] = $element['#attributes']['id']; | |
drupal_attach_tabledrag($element, $options); | |
} | |
} | |
return $element; | |
} | |
} |