Table.php 13.3 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
<?php

/**
 * @file
 * Contains \Drupal\Core\Render\Element\Table.
 */

namespace Drupal\Core\Render\Element;

use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;

/**
 * Provides a render element for a table.
 *
 * Note: Although this extends FormElement, it can be used outside the
 * context of a form.
 *
 * @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
50 51
      // drupal_attach_tabledrag(). Table::preRenderTable() prepends the HTML ID
      // of the table to each set of options.
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
      // @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.
127 128 129
        // 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.
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221
        $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_html_id() would automatically
        // append a suffix to the tableselect form element's #id otherwise.
        $row['#id'] = drupal_html_id('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' => drupal_html_id('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.
222 223
    $triggering_element = $form_state->getTriggeringElement();
    if (empty($triggering_element['#tableselect'])) {
224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359
      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 into #rows suitable for theme_table().
   *
   * This function converts sub-elements of an element of #type 'table' to be
   * suitable for theme_table():
   * - 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'),
   *     '#href' => 'thing/' . $row . '/edit',
   *   );
   * }
   * @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 theme_table()
   * @see drupal_process_attached()
   * @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 http://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;
  }

}