Chris@0: []]; Chris@0: $info['#theme'] = 'field_ui_table'; Chris@0: // Prepend FieldUiTable's prerender callbacks. Chris@0: array_unshift($info['#pre_render'], [$this, 'tablePreRender'], [$this, 'preRenderRegionRows']); Chris@0: return $info; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Performs pre-render tasks on field_ui_table elements. Chris@0: * Chris@0: * @param array $elements Chris@0: * A structured array containing two sub-levels of elements. Properties Chris@0: * used: Chris@0: * - #tabledrag: The value is a list of $options arrays that are passed to Chris@0: * drupal_attach_tabledrag(). The HTML ID of the table is added to each Chris@0: * $options array. Chris@0: * Chris@0: * @return array Chris@0: * The $element with prepared variables ready for field-ui-table.html.twig. Chris@0: * Chris@16: * @see \Drupal\Core\Render\RendererInterface::render() Chris@0: * @see \Drupal\Core\Render\Element\Table::preRenderTable() Chris@0: */ Chris@0: public static function tablePreRender($elements) { Chris@0: $js_settings = []; Chris@0: Chris@0: // For each region, build the tree structure from the weight and parenting Chris@0: // data contained in the flat form structure, to determine row order and Chris@0: // indentation. Chris@0: $regions = $elements['#regions']; Chris@0: $tree = ['' => ['name' => '', 'children' => []]]; Chris@0: $trees = array_fill_keys(array_keys($regions), $tree); Chris@0: Chris@0: $parents = []; Chris@0: $children = Element::children($elements); Chris@0: $list = array_combine($children, $children); Chris@0: Chris@0: // Iterate on rows until we can build a known tree path for all of them. Chris@0: while ($list) { Chris@0: foreach ($list as $name) { Chris@0: $row = &$elements[$name]; Chris@0: $parent = $row['parent_wrapper']['parent']['#value']; Chris@0: // Proceed if parent is known. Chris@0: if (empty($parent) || isset($parents[$parent])) { Chris@0: // Grab parent, and remove the row from the next iteration. Chris@0: $parents[$name] = $parent ? array_merge($parents[$parent], [$parent]) : []; Chris@0: unset($list[$name]); Chris@0: Chris@0: // Determine the region for the row. Chris@0: $region_name = call_user_func_array($row['#region_callback'], [&$row]); Chris@0: Chris@0: // Add the element in the tree. Chris@0: $target = &$trees[$region_name]['']; Chris@0: foreach ($parents[$name] as $key) { Chris@0: $target = &$target['children'][$key]; Chris@0: } Chris@0: $target['children'][$name] = ['name' => $name, 'weight' => $row['weight']['#value']]; Chris@0: Chris@0: // Add tabledrag indentation to the first row cell. Chris@0: if ($depth = count($parents[$name])) { Chris@0: $children = Element::children($row); Chris@0: $cell = current($children); Chris@0: $indentation = [ Chris@0: '#theme' => 'indentation', Chris@0: '#size' => $depth, Chris@0: '#suffix' => isset($row[$cell]['#prefix']) ? $row[$cell]['#prefix'] : '', Chris@0: ]; Chris@0: $row[$cell]['#prefix'] = \Drupal::service('renderer')->render($indentation); Chris@0: } Chris@0: Chris@0: // Add row id and associate JS settings. Chris@0: $id = Html::getClass($name); Chris@0: $row['#attributes']['id'] = $id; Chris@0: if (isset($row['#js_settings'])) { Chris@0: $row['#js_settings'] += [ Chris@0: 'rowHandler' => $row['#row_type'], Chris@0: 'name' => $name, Chris@0: 'region' => $region_name, Chris@0: ]; Chris@0: $js_settings[$id] = $row['#js_settings']; Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: // Determine rendering order from the tree structure. Chris@0: foreach ($regions as $region_name => $region) { Chris@0: $elements['#regions'][$region_name]['rows_order'] = array_reduce($trees[$region_name], [static::class, 'reduceOrder']); Chris@0: } Chris@0: Chris@0: $elements['#attached']['drupalSettings']['fieldUIRowsData'] = $js_settings; Chris@0: Chris@0: // If the custom #tabledrag is set and there is a HTML ID, add the table's Chris@0: // HTML ID to the options and attach the behavior. Chris@0: // @see \Drupal\Core\Render\Element\Table::preRenderTable() Chris@0: if (!empty($elements['#tabledrag']) && isset($elements['#attributes']['id'])) { Chris@0: foreach ($elements['#tabledrag'] as $options) { Chris@0: $options['table_id'] = $elements['#attributes']['id']; Chris@0: drupal_attach_tabledrag($elements, $options); Chris@0: } Chris@0: } Chris@0: Chris@0: return $elements; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Performs pre-render to move #regions to rows. Chris@0: * Chris@0: * @param array $elements Chris@0: * A structured array containing two sub-levels of elements. Properties Chris@0: * used: Chris@0: * - #tabledrag: The value is a list of $options arrays that are passed to Chris@0: * drupal_attach_tabledrag(). The HTML ID of the table is added to each Chris@0: * $options array. Chris@0: * Chris@0: * @return array Chris@0: * The $element with prepared variables ready for field-ui-table.html.twig. Chris@0: */ Chris@0: public static function preRenderRegionRows($elements) { Chris@0: // Determine the colspan to use for region rows, by checking the number of Chris@0: // columns in the headers. Chris@0: $columns_count = 0; Chris@0: foreach ($elements['#header'] as $header) { Chris@0: $columns_count += (is_array($header) && isset($header['colspan']) ? $header['colspan'] : 1); Chris@0: } Chris@0: Chris@0: $rows = []; Chris@0: foreach (Element::children($elements) as $key) { Chris@0: $rows[$key] = $elements[$key]; Chris@0: unset($elements[$key]); Chris@0: } Chris@0: Chris@0: // Render rows, region by region. Chris@0: foreach ($elements['#regions'] as $region_name => $region) { Chris@0: $region_name_class = Html::getClass($region_name); Chris@0: Chris@0: // Add region rows. Chris@0: if (isset($region['title']) && empty($region['invisible'])) { Chris@0: $elements['#rows'][] = [ Chris@0: 'class' => [ Chris@0: 'region-title', Chris@17: 'region-' . $region_name_class . '-title', Chris@0: ], Chris@0: 'no_striping' => TRUE, Chris@0: 'data' => [ Chris@0: ['data' => $region['title'], 'colspan' => $columns_count], Chris@0: ], Chris@0: ]; Chris@0: } Chris@0: if (isset($region['message'])) { Chris@0: $class = (empty($region['rows_order']) ? 'region-empty' : 'region-populated'); Chris@0: $elements['#rows'][] = [ Chris@0: 'class' => [ Chris@0: 'region-message', Chris@0: 'region-' . $region_name_class . '-message', $class, Chris@0: ], Chris@0: 'no_striping' => TRUE, Chris@0: 'data' => [ Chris@0: ['data' => $region['message'], 'colspan' => $columns_count], Chris@0: ], Chris@0: ]; Chris@0: } Chris@0: Chris@0: // Add form rows, in the order determined at pre-render time. Chris@0: foreach ($region['rows_order'] as $name) { Chris@0: $element = $rows[$name]; Chris@0: Chris@0: $row = ['data' => []]; Chris@0: if (isset($element['#attributes'])) { Chris@0: $row += $element['#attributes']; Chris@0: } Chris@0: Chris@0: // Render children as table cells. Chris@0: foreach (Element::children($element) as $cell_key) { Chris@0: $child = $element[$cell_key]; Chris@0: // Do not render a cell for children of #type 'value'. Chris@0: if (!(isset($child['#type']) && $child['#type'] == 'value')) { Chris@0: $cell = ['data' => $child]; Chris@0: if (isset($child['#cell_attributes'])) { Chris@0: $cell += $child['#cell_attributes']; Chris@0: } Chris@0: $row['data'][] = $cell; Chris@0: } Chris@0: } Chris@0: $elements['#rows'][] = $row; Chris@0: } Chris@0: } Chris@0: Chris@0: return $elements; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Determines the rendering order of an array representing a tree. Chris@0: * Chris@0: * Callback for array_reduce() within ::tablePreRender(). Chris@0: * Chris@0: * @param mixed $array Chris@0: * Holds the return value of the previous iteration; in the case of the Chris@0: * first iteration it instead holds the value of the initial array. Chris@0: * @param mixed $a Chris@0: * Holds the value of the current iteration. Chris@0: * Chris@0: * @return array Chris@0: * Array where rendering order has been determined. Chris@0: */ Chris@0: public static function reduceOrder($array, $a) { Chris@0: $array = !$array ? [] : $array; Chris@0: if ($a['name']) { Chris@0: $array[] = $a['name']; Chris@0: } Chris@0: if (!empty($a['children'])) { Chris@0: uasort($a['children'], ['Drupal\Component\Utility\SortArray', 'sortByWeightElement']); Chris@0: $array = array_merge($array, array_reduce($a['children'], [static::class, 'reduceOrder'])); Chris@0: } Chris@0: Chris@0: return $array; Chris@0: } Chris@0: Chris@0: }