Table.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. <?php
  2. namespace Drupal\Core\Render\Element;
  3. use Drupal\Core\Form\FormStateInterface;
  4. use Drupal\Core\Render\Element;
  5. use Drupal\Component\Utility\Html as HtmlUtility;
  6. /**
  7. * Provides a render element for a table.
  8. *
  9. * Note: Although this extends FormElement, it can be used outside the
  10. * context of a form.
  11. *
  12. * Properties:
  13. * - #header: An array of table header labels.
  14. * - #rows: An array of the rows to be displayed. Each row is either an array
  15. * of cell contents or an array of properties as described in table.html.twig
  16. * Alternatively specify the data for the table as child elements of the table
  17. * element. Table elements would contain rows elements that would in turn
  18. * contain column elements.
  19. * - #empty: Text to display when no rows are present.
  20. * - #responsive: Indicates whether to add the drupal.responsive_table library
  21. * providing responsive tables. Defaults to TRUE.
  22. * - #sticky: Indicates whether to add the drupal.tableheader library that makes
  23. * table headers always visible at the top of the page. Defaults to FALSE.
  24. * - #size: The size of the input element in characters.
  25. *
  26. * Usage example:
  27. * @code
  28. * $form['contacts'] = array(
  29. * '#type' => 'table',
  30. * '#caption' => $this->t('Sample Table'),
  31. * '#header' => array($this->t('Name'), $this->t('Phone')),
  32. * );
  33. *
  34. * for ($i = 1; $i <= 4; $i++) {
  35. * $form['contacts'][$i]['#attributes'] = array('class' => array('foo', 'baz'));
  36. * $form['contacts'][$i]['name'] = array(
  37. * '#type' => 'textfield',
  38. * '#title' => $this->t('Name'),
  39. * '#title_display' => 'invisible',
  40. * );
  41. *
  42. * $form['contacts'][$i]['phone'] = array(
  43. * '#type' => 'tel',
  44. * '#title' => $this->t('Phone'),
  45. * '#title_display' => 'invisible',
  46. * );
  47. * }
  48. *
  49. * $form['contacts'][]['colspan_example'] = array(
  50. * '#plain_text' => 'Colspan Example',
  51. * '#wrapper_attributes' => array('colspan' => 2, 'class' => array('foo', 'bar')),
  52. * );
  53. * @endcode
  54. * @see \Drupal\Core\Render\Element\Tableselect
  55. *
  56. * @FormElement("table")
  57. */
  58. class Table extends FormElement {
  59. /**
  60. * {@inheritdoc}
  61. */
  62. public function getInfo() {
  63. $class = get_class($this);
  64. return [
  65. '#header' => [],
  66. '#rows' => [],
  67. '#empty' => '',
  68. // Properties for tableselect support.
  69. '#input' => TRUE,
  70. '#tree' => TRUE,
  71. '#tableselect' => FALSE,
  72. '#sticky' => FALSE,
  73. '#responsive' => TRUE,
  74. '#multiple' => TRUE,
  75. '#js_select' => TRUE,
  76. '#process' => [
  77. [$class, 'processTable'],
  78. ],
  79. '#element_validate' => [
  80. [$class, 'validateTable'],
  81. ],
  82. // Properties for tabledrag support.
  83. // The value is a list of arrays that are passed to
  84. // drupal_attach_tabledrag(). Table::preRenderTable() prepends the HTML ID
  85. // of the table to each set of options.
  86. // @see drupal_attach_tabledrag()
  87. '#tabledrag' => [],
  88. // Render properties.
  89. '#pre_render' => [
  90. [$class, 'preRenderTable'],
  91. ],
  92. '#theme' => 'table',
  93. ];
  94. }
  95. /**
  96. * {@inheritdoc}
  97. */
  98. public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
  99. // If #multiple is FALSE, the regular default value of radio buttons is used.
  100. if (!empty($element['#tableselect']) && !empty($element['#multiple'])) {
  101. // Contrary to #type 'checkboxes', the default value of checkboxes in a
  102. // table is built from the array keys (instead of array values) of the
  103. // #default_value property.
  104. // @todo D8: Remove this inconsistency.
  105. if ($input === FALSE) {
  106. $element += ['#default_value' => []];
  107. $value = array_keys(array_filter($element['#default_value']));
  108. return array_combine($value, $value);
  109. }
  110. else {
  111. return is_array($input) ? array_combine($input, $input) : [];
  112. }
  113. }
  114. }
  115. /**
  116. * #process callback for #type 'table' to add tableselect support.
  117. *
  118. * @param array $element
  119. * An associative array containing the properties and children of the
  120. * table element.
  121. * @param \Drupal\Core\Form\FormStateInterface $form_state
  122. * The current state of the form.
  123. * @param array $complete_form
  124. * The complete form structure.
  125. *
  126. * @return array
  127. * The processed element.
  128. */
  129. public static function processTable(&$element, FormStateInterface $form_state, &$complete_form) {
  130. if ($element['#tableselect']) {
  131. if ($element['#multiple']) {
  132. $value = is_array($element['#value']) ? $element['#value'] : [];
  133. }
  134. // Advanced selection behavior makes no sense for radios.
  135. else {
  136. $element['#js_select'] = FALSE;
  137. }
  138. // Add a "Select all" checkbox column to the header.
  139. // @todo D8: Rename into #select_all?
  140. if ($element['#js_select']) {
  141. $element['#attached']['library'][] = 'core/drupal.tableselect';
  142. array_unshift($element['#header'], ['class' => ['select-all']]);
  143. }
  144. // Add an empty header column for radio buttons or when a "Select all"
  145. // checkbox is not desired.
  146. else {
  147. array_unshift($element['#header'], '');
  148. }
  149. if (!isset($element['#default_value']) || $element['#default_value'] === 0) {
  150. $element['#default_value'] = [];
  151. }
  152. // Create a checkbox or radio for each row in a way that the value of the
  153. // tableselect element behaves as if it had been of #type checkboxes or
  154. // radios.
  155. foreach (Element::children($element) as $key) {
  156. $row = &$element[$key];
  157. // Prepare the element #parents for the tableselect form element.
  158. // Their values have to be located in child keys (#tree is ignored),
  159. // since Table::validateTable() has to be able to validate whether input
  160. // (for the parent #type 'table' element) has been submitted.
  161. $element_parents = array_merge($element['#parents'], [$key]);
  162. // Since the #parents of the tableselect form element will equal the
  163. // #parents of the row element, prevent FormBuilder from auto-generating
  164. // an #id for the row element, since
  165. // \Drupal\Component\Utility\Html::getUniqueId() would automatically
  166. // append a suffix to the tableselect form element's #id otherwise.
  167. $row['#id'] = HtmlUtility::getUniqueId('edit-' . implode('-', $element_parents) . '-row');
  168. // Do not overwrite manually created children.
  169. if (!isset($row['select'])) {
  170. // Determine option label; either an assumed 'title' column, or the
  171. // first available column containing a #title or #markup.
  172. // @todo Consider to add an optional $element[$key]['#title_key']
  173. // defaulting to 'title'?
  174. unset($label_element);
  175. $title = NULL;
  176. if (isset($row['title']['#type']) && $row['title']['#type'] == 'label') {
  177. $label_element = &$row['title'];
  178. }
  179. else {
  180. if (!empty($row['title']['#title'])) {
  181. $title = $row['title']['#title'];
  182. }
  183. else {
  184. foreach (Element::children($row) as $column) {
  185. if (isset($row[$column]['#title'])) {
  186. $title = $row[$column]['#title'];
  187. break;
  188. }
  189. if (isset($row[$column]['#markup'])) {
  190. $title = $row[$column]['#markup'];
  191. break;
  192. }
  193. }
  194. }
  195. if (isset($title) && $title !== '') {
  196. $title = t('Update @title', ['@title' => $title]);
  197. }
  198. }
  199. // Prepend the select column to existing columns.
  200. $row = ['select' => []] + $row;
  201. $row['select'] += [
  202. '#type' => $element['#multiple'] ? 'checkbox' : 'radio',
  203. '#id' => HtmlUtility::getUniqueId('edit-' . implode('-', $element_parents)),
  204. // @todo If rows happen to use numeric indexes instead of string keys,
  205. // this results in a first row with $key === 0, which is always FALSE.
  206. '#return_value' => $key,
  207. '#attributes' => $element['#attributes'],
  208. '#wrapper_attributes' => [
  209. 'class' => ['table-select'],
  210. ],
  211. ];
  212. if ($element['#multiple']) {
  213. $row['select']['#default_value'] = isset($value[$key]) ? $key : NULL;
  214. $row['select']['#parents'] = $element_parents;
  215. }
  216. else {
  217. $row['select']['#default_value'] = ($element['#default_value'] == $key ? $key : NULL);
  218. $row['select']['#parents'] = $element['#parents'];
  219. }
  220. if (isset($label_element)) {
  221. $label_element['#id'] = $row['select']['#id'] . '--label';
  222. $label_element['#for'] = $row['select']['#id'];
  223. $row['select']['#attributes']['aria-labelledby'] = $label_element['#id'];
  224. $row['select']['#title_display'] = 'none';
  225. }
  226. else {
  227. $row['select']['#title'] = $title;
  228. $row['select']['#title_display'] = 'invisible';
  229. }
  230. }
  231. }
  232. }
  233. return $element;
  234. }
  235. /**
  236. * #element_validate callback for #type 'table'.
  237. *
  238. * @param array $element
  239. * An associative array containing the properties and children of the
  240. * table element.
  241. * @param \Drupal\Core\Form\FormStateInterface $form_state
  242. * The current state of the form.
  243. * @param array $complete_form
  244. * The complete form structure.
  245. */
  246. public static function validateTable(&$element, FormStateInterface $form_state, &$complete_form) {
  247. // Skip this validation if the button to submit the form does not require
  248. // selected table row data.
  249. $triggering_element = $form_state->getTriggeringElement();
  250. if (empty($triggering_element['#tableselect'])) {
  251. return;
  252. }
  253. if ($element['#multiple']) {
  254. if (!is_array($element['#value']) || !count(array_filter($element['#value']))) {
  255. $form_state->setError($element, t('No items selected.'));
  256. }
  257. }
  258. elseif (!isset($element['#value']) || $element['#value'] === '') {
  259. $form_state->setError($element, t('No item selected.'));
  260. }
  261. }
  262. /**
  263. * #pre_render callback to transform children of an element of #type 'table'.
  264. *
  265. * This function converts sub-elements of an element of #type 'table' to be
  266. * suitable for table.html.twig:
  267. * - The first level of sub-elements are table rows. Only the #attributes
  268. * property is taken into account.
  269. * - The second level of sub-elements is converted into columns for the
  270. * corresponding first-level table row.
  271. *
  272. * Simple example usage:
  273. * @code
  274. * $form['table'] = array(
  275. * '#type' => 'table',
  276. * '#header' => array($this->t('Title'), array('data' => $this->t('Operations'), 'colspan' => '1')),
  277. * // Optionally, to add tableDrag support:
  278. * '#tabledrag' => array(
  279. * array(
  280. * 'action' => 'order',
  281. * 'relationship' => 'sibling',
  282. * 'group' => 'thing-weight',
  283. * ),
  284. * ),
  285. * );
  286. * foreach ($things as $row => $thing) {
  287. * $form['table'][$row]['#weight'] = $thing['weight'];
  288. *
  289. * $form['table'][$row]['title'] = array(
  290. * '#type' => 'textfield',
  291. * '#default_value' => $thing['title'],
  292. * );
  293. *
  294. * // Optionally, to add tableDrag support:
  295. * $form['table'][$row]['#attributes']['class'][] = 'draggable';
  296. * $form['table'][$row]['weight'] = array(
  297. * '#type' => 'textfield',
  298. * '#title' => $this->t('Weight for @title', array('@title' => $thing['title'])),
  299. * '#title_display' => 'invisible',
  300. * '#size' => 4,
  301. * '#default_value' => $thing['weight'],
  302. * '#attributes' => array('class' => array('thing-weight')),
  303. * );
  304. *
  305. * // The amount of link columns should be identical to the 'colspan'
  306. * // attribute in #header above.
  307. * $form['table'][$row]['edit'] = array(
  308. * '#type' => 'link',
  309. * '#title' => $this->t('Edit'),
  310. * '#url' => Url::fromRoute('entity.test_entity.edit_form', ['test_entity' => $row]),
  311. * );
  312. * }
  313. * @endcode
  314. *
  315. * @param array $element
  316. * A structured array containing two sub-levels of elements. Properties used:
  317. * - #tabledrag: The value is a list of $options arrays that are passed to
  318. * drupal_attach_tabledrag(). The HTML ID of the table is added to each
  319. * $options array.
  320. *
  321. * @return array
  322. *
  323. * @see template_preprocess_table()
  324. * @see \Drupal\Core\Render\AttachmentsResponseProcessorInterface::processAttachments()
  325. * @see drupal_attach_tabledrag()
  326. */
  327. public static function preRenderTable($element) {
  328. foreach (Element::children($element) as $first) {
  329. $row = ['data' => []];
  330. // Apply attributes of first-level elements as table row attributes.
  331. if (isset($element[$first]['#attributes'])) {
  332. $row += $element[$first]['#attributes'];
  333. }
  334. // Turn second-level elements into table row columns.
  335. // @todo Do not render a cell for children of #type 'value'.
  336. // @see https://www.drupal.org/node/1248940
  337. foreach (Element::children($element[$first]) as $second) {
  338. // Assign the element by reference, so any potential changes to the
  339. // original element are taken over.
  340. $column = ['data' => &$element[$first][$second]];
  341. // Apply wrapper attributes of second-level elements as table cell
  342. // attributes.
  343. if (isset($element[$first][$second]['#wrapper_attributes'])) {
  344. $column += $element[$first][$second]['#wrapper_attributes'];
  345. }
  346. $row['data'][] = $column;
  347. }
  348. $element['#rows'][] = $row;
  349. }
  350. // Take over $element['#id'] as HTML ID attribute, if not already set.
  351. Element::setAttributes($element, ['id']);
  352. // Add sticky headers, if applicable.
  353. if (count($element['#header']) && $element['#sticky']) {
  354. $element['#attached']['library'][] = 'core/drupal.tableheader';
  355. // Add 'sticky-enabled' class to the table to identify it for JS.
  356. // This is needed to target tables constructed by this function.
  357. $element['#attributes']['class'][] = 'sticky-enabled';
  358. }
  359. // If the table has headers and it should react responsively to columns hidden
  360. // with the classes represented by the constants RESPONSIVE_PRIORITY_MEDIUM
  361. // and RESPONSIVE_PRIORITY_LOW, add the tableresponsive behaviors.
  362. if (count($element['#header']) && $element['#responsive']) {
  363. $element['#attached']['library'][] = 'core/drupal.tableresponsive';
  364. // Add 'responsive-enabled' class to the table to identify it for JS.
  365. // This is needed to target tables constructed by this function.
  366. $element['#attributes']['class'][] = 'responsive-enabled';
  367. }
  368. // If the custom #tabledrag is set and there is a HTML ID, add the table's
  369. // HTML ID to the options and attach the behavior.
  370. if (!empty($element['#tabledrag']) && isset($element['#attributes']['id'])) {
  371. foreach ($element['#tabledrag'] as $options) {
  372. $options['table_id'] = $element['#attributes']['id'];
  373. drupal_attach_tabledrag($element, $options);
  374. }
  375. }
  376. return $element;
  377. }
  378. }