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