You are here

bricks.module in Bricks​ 2.x

Same filename and directory in other branches
  1. 8 bricks.module
  2. 7.5 bricks.module
  3. 7.4 bricks.module

File

bricks.module
View source
<?php

use Drupal\bricks\BricksFieldItemInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Component\Utility\Html;

/**
 * When rearranging a list of entity view render arrays into layouts, a list
 * of parents are kept and those parents are nonnegative integers so the root
 * is an index which can't occur otherwise.
 */
const BRICKS_TREE_ROOT = -1;

/**
 * Prepares variables for `field.html.twig`.
 */
function bricks_preprocess_field(&$variables) {
  $element = $variables['element'];
  if (substr($element['#formatter'], 0, 7) === 'bricks_') {
    $variables['items'] = [
      [
        'content' => _bricks_nest_items($variables['items']),
      ],
    ];
  }
}

/**
 * Helper function: converts element's items to a tree structure.
 *
 * To understand how this function converts a flat list of render elements
 * into a tree, search this function and _bricks_nested_get_new_elements for
 * magic.
 */
function _bricks_nest_items($render_elements) {

  // Every element in $new_elements is a render array to view an entity plus a
  // few bricks specific extras: some CSS classes are added, the entity label
  // is surfaced and most importantly #bricks_parent_key points to the parent
  // in the $new_elements array and #layout is extracted from the field item
  // options.
  $new_elements = _bricks_nested_get_new_elements($render_elements);

  // Process the elements in reverse order: See magic #3 why.
  foreach (array_reverse(array_keys($new_elements)) as $key) {

    // Save the parent key because moving into a layout loses it.
    $parent_key = $new_elements[$key]['#bricks_parent_key'];

    // If this is a layout paragraph, move the children of it into a layout.
    if (!empty($new_elements[$key]['#layout']) && \Drupal::service('module_handler')
      ->moduleExists('layout_discovery')) {
      $keep = array_intersect_key($new_elements[$key], array_flip([
        '#label',
        '#attributes',
        '#paragraph',
      ]));
      $new_elements[$key] = $keep + _bricks_build_layout_from_items($new_elements[$key]['#layout'], $new_elements[$key]['layout_children']);
    }

    // Magic #3. If this is not a root element, move it under the parent.
    // By processing the elements from the bottom, by the time the parent is
    // reached, all the children is moved under it.
    if ($parent_key !== BRICKS_TREE_ROOT) {
      array_unshift($new_elements[$parent_key]['layout_children'], $new_elements[$key]);
      unset($new_elements[$key]);
    }
  }
  return $new_elements;
}

/**
 * @param array $render_elements
 *
 * @return array
 */
function _bricks_nested_get_new_elements(array $render_elements) : array {

  // A stack of parents, it contains a flat list of parents (also called
  // ancestors).
  $parents = [
    BRICKS_TREE_ROOT,
  ];
  $previous_depth = 0;
  foreach ($render_elements as $key => $render_element) {

    // At this point, the element contains a 'content' key containing a render
    // array to view an entity and an empty attributes object. Remove this
    // layer and keep only content.
    $content = $render_element['content'] ?? [];

    // Now, find the field reference item referencing the entity in content.
    $field_item = _bricks_nest_get_field_item($content);

    // Sanity check.
    if (!$field_item) {
      continue;
    }

    // The bricks tree widget records the indentation of the item as depth.
    $depth = $field_item
      ->getDepth();

    // Magic #1a. if the current depth is higher than the previous one then the
    // previous key is the current parent. This is the equivalent of dragging
    // an element in the widget to the right. The current parent is always at
    // index 0, the grandparent is at index 1 etc.
    if ($depth > $previous_depth) {
      array_unshift($parents, $previous_key);
    }
    elseif ($depth < $previous_depth) {
      for ($i = 0; $i < $previous_depth - $depth; $i++) {
        array_shift($parents);
      }
    }
    $previous_depth = $depth;
    $previous_key = $key;

    // Magic #2. Put $parents[0] in #bricks_parent_key (and add the other
    // extras). The function itself is not doing anything magical.
    $new_elements[$key] = _bricks_nest_get_new_element($content, $key, $field_item, $parents[0]);
  }
  return $new_elements ?? [];
}

/**
 * Assemble a render element based on existing content and field item data.
 */
function _bricks_nest_get_new_element($content, int $item_delta, BricksFieldItemInterface $field_item, $parent) : array {
  $element = $content;
  $entity = $field_item->entity;
  $element['#label'] = $entity
    ->label();
  $element['#bricks_parent_key'] = $parent;
  $element['#attributes']['class'][] = 'brick';
  $element['#attributes']['class'][] = 'brick--type--' . Html::cleanCssIdentifier($entity
    ->bundle());
  $element['#attributes']['class'][] = 'brick--id--' . $entity
    ->id();
  $element['layout_children'] = [];
  if ($view_mode = $field_item
    ->getOption('view_mode')) {
    $element['#view_mode'] = $view_mode;
  }
  if ($layout = $field_item
    ->getOption('layout')) {
    $element['#layout'] = $layout;
  }
  if ($css_class = $field_item
    ->getOption('css_class')) {
    $element['#attributes']['class'][] = $css_class;
  }
  return $element;
}

/**
 * Helper to get the field item list a render array is made from.
 *
 * @param $content
 *   A render array to view an entity.
 *
 * @return
 *   The bricks field item.
 */
function _bricks_nest_get_field_item(array $content) : ?BricksFieldItemInterface {
  $entity = NULL;

  // The default is the same #theme and entity type id, see
  // EntityViewBuilder::getBuildDefaults().
  if ($theme = $content['#theme'] ?? '') {
    $entity = $content["#{$theme}"] ?? NULL;
  }
  if (!$entity) {

    // If that didn't work out, try to fish for it among the properties. If
    // there is only one entity sure it is the one. If there is more than one,
    // give up.
    foreach (Element::properties($content) as $property) {
      if ($content[$property] instanceof EntityInterface) {
        if ($entity) {
          throw new \LogicException(sprintf('Unsupported render array with entity types %s %s', $entity
            ->getEntityTypeId(), $content[$property]
            ->getEntityTypeId()));
        }
        $entity = $content[$property];
      }
    }
  }
  return $entity ? $entity->_referringItem : NULL;
}

/**
 * Helper function for layout handling in _bricks_nest_items().
 */
function _bricks_build_layout_from_items($layout, $items) {
  $layoutPluginManager = \Drupal::service('plugin.manager.core.layout');
  if (!$layoutPluginManager
    ->hasDefinition($layout)) {
    \Drupal::messenger()
      ->addWarning(t('Layout `%layout_id` is unknown.', [
      '%layout_id' => $layout,
    ]));
    return [];
  }

  // Provide any configuration to the layout plugin if necessary.
  $layoutInstance = $layoutPluginManager
    ->createInstance($layout);
  $regionNames = $layoutInstance
    ->getPluginDefinition()
    ->getRegionNames();
  $defaultRegion = $layoutInstance
    ->getPluginDefinition()
    ->getDefaultRegion();

  // If there is just one region and is the default one,
  // add all items inside the default region.
  $use_default_region = count($regionNames) == 1 && !empty($defaultRegion);
  $regions = [];
  if ($use_default_region) {
    $regions[$defaultRegion] = $items;
  }
  else {

    // Adjust the lengths.
    $count = min(count($regionNames), count($items));
    $regionNamesSlice = array_slice($regionNames, 0, $count);
    $items = array_slice($items, 0, $count);

    // Build the content for your regions.
    $regions = array_combine($regionNamesSlice, $items);
  }

  // This builds the render array.
  return $layoutInstance
    ->build($regions);
}

/* BRICKS EDITING */

/**
 * Implements hook_field_widget_info_alter().
 */
function bricks_field_widget_info_alter(array &$info) {

  // Let Bricks to re-use ANY Entity Reference -compatible widgets:
  foreach ($info as $widget_id => &$widget_info) {
    if (in_array('entity_reference', $widget_info['field_types'])) {
      $widget_info['field_types'][] = 'bricks';
    }
    if (in_array('entity_reference_revisions', $widget_info['field_types'])) {
      $widget_info['field_types'][] = 'bricks_revisioned';
    }
  }
}

/**
 * Implements hook_field_widget_WIDGET_TYPE_form_alter() for `entity_reference_autocomplete`.
 */
function bricks_field_widget_entity_reference_autocomplete_form_alter(&$element, FormStateInterface $form_state, $context) {
  $field_type = $context['items']
    ->getFieldDefinition()
    ->getType();

  // @TODO: Replace by 'Nested bricks' widget setting.
  if (in_array($field_type, [
    'bricks',
  ])) {

    // @TODO: Find a better way to be used in _bricks_preprocess_tabledrag_form().
    $element['#widget'] = 'entity_reference_autocomplete';

    // #default_value is en Entity or NULL.
    _bricks_form_element_alter($element, $context['items'][$context['delta']], $element['target_id']['#default_value']);
    hide($element['depth']);
  }
}

/**
 * Implements hook_field_widget_WIDGET_TYPE_form_alter() for `bricks_tree_autocomplete`.
 */
function bricks_field_widget_bricks_tree_autocomplete_form_alter(&$element, FormStateInterface $form_state, $context) {
  bricks_field_widget_entity_reference_autocomplete_form_alter($element, $form_state, $context);
}

/**
 * Implements hook_field_widget_WIDGET_TYPE_form_alter() for `paragraphs`.
 */
function bricks_field_widget_paragraphs_form_alter(&$element, FormStateInterface $form_state, $context) {
  $field_type = $context['items']
    ->getFieldDefinition()
    ->getType();

  // @TODO: Replace by 'Nested bricks' widget setting.
  if (in_array($field_type, [
    'bricks_revisioned',
  ])) {

    // @TODO: Find a better way to be used in _bricks_preprocess_tabledrag_form().
    $element['#widget'] = 'paragraphs';
    $item = $context['items'][$context['delta']];
    $entity = $item->target_id ? \Drupal::entityTypeManager()
      ->getStorage('paragraph')
      ->load($item->target_id) : Paragraph::create([
      'type' => $element['subform']['#process'][0][0]
        ->getTargetBundle(),
    ]);
    _bricks_form_element_alter($element, $item, $entity);
    hide($element['depth']);
  }
}

/**
 * Prepares variables for `field-multiple-value-form.html.twig`.
 */
function bricks_preprocess_field_multiple_value_form(&$variables) {
  _bricks_preprocess_tabledrag_form($variables, 'element', 'entity_reference_autocomplete', $variables['element']['#field_name'] . '-delta-order');
  _bricks_preprocess_tabledrag_form($variables, 'element', 'paragraphs', $variables['element']['#field_name'] . '-delta-order');
}

/**
 * Helper function for hook_preprocess_field_multiple_value_form().
 */
function _bricks_preprocess_tabledrag_form(&$variables, $element_key, $widget, $order_class, $render_options = FALSE) {
  $element = $variables[$element_key];
  $operation_key = NULL;

  // @TODO: Replace by 'Nested bricks' widget setting.
  if (isset($element['#widget']) && $element['#widget'] == $widget || isset($element[0]['#widget']) && $element[0]['#widget'] == $widget) {

    // @TODO: Tmp hack for the proper indent width calculation.
    $variables['table']['#header'][0]['style'] = 'min-width: 150px';
    $variables['table']['#header'][] = [
      'data' => t('Depth'),
      'class' => [
        'bricks-depth-header',
      ],
    ];
    if ($render_options) {

      // Find Operations column
      $operation_key = array_filter($variables['table']['#header'], function ($item) {
        return isset($item['is_operation']);
      });
      if (!empty($operation_key) && is_array($operation_key)) {
        $operation_key = array_keys($operation_key);
        $operation_key = array_pop($operation_key);

        // Insert new options column before operations.
        array_splice($variables['table']['#header'], $operation_key, 0, [
          [
            'data' => t('Options'),
          ],
        ]);
      }
    }
    $row = 0;
    foreach (Element::children($element) as $i => $key) {
      if ($key !== 'add_more' && $key !== 'header_actions') {
        $depth = $element[$key]['depth']['#value'];
        $indentation = [];
        if ($depth > 0) {
          $indentation = [
            '#theme' => 'indentation',
            '#size' => $depth,
          ];
        }
        $drag_cell =& $variables['table']['#rows'][$row]['data'][0];
        $drag_cell['data'] = !empty($indentation) ? \Drupal::service('renderer')
          ->render($indentation) : '' . $drag_cell['data'];

        // @TODO
        $drag_cell['style'] = 'width: auto; min-width: 150px';
        show($element[$key]['depth']);
        $variables['table']['#rows'][$row]['data'][] = \Drupal::service('renderer')
          ->render($element[$key]['depth']);
        if ($render_options && !is_null($operation_key)) {

          // Insert data row in options column.
          array_splice($variables['table']['#rows'][$row]['data'], $operation_key, 0, [
            [
              'data' => \Drupal::service('renderer')
                ->render($element[$key]['options']),
              'class' => 'inline-entity-form-brick-options',
            ],
          ]);
        }
      }
      if ($key !== 'add_more') {
        $row++;
      }
    }
    $tabledrag_options =& $variables['table']['#tabledrag'];
    $tabledrag_options[0]['relationship'] = 'all';
    $tabledrag_options[] = [
      'action' => 'depth',
      'relationship' => 'group',
      'group' => 'bricks-depth',
    ];

    // Fake option to enable indentation:
    $tabledrag_options[] = [
      'action' => 'match',
      'relationship' => 'parent',
      'group' => $order_class,
    ];
    $variables['table']['#attached']['library'][] = 'bricks/tabledrag.relationship-all';
  }
}

/**
 * Helper function for widget's formElement().
 */
function _bricks_form_element_alter(&$element, $item, $entity) {
  $element['depth'] = [
    // @TODO: Other types break the correct indentations.
    '#type' => 'hidden',
    '#default_value' => !empty($item->depth) ? $item->depth : 0,
    '#weight' => 10,
    '#attributes' => [
      'class' => [
        'bricks-depth',
      ],
    ],
  ];
  $element['options'] = [
    '#type' => 'container',
    '#weight' => 100,
    '#attributes' => [
      'class' => [
        'container-inline',
      ],
    ],
  ];
  if ($entity) {
    if ($entity
      ->bundle() == 'layout' && \Drupal::service('module_handler')
      ->moduleExists('layout_discovery')) {
      $element['options']['layout'] = [
        '#type' => 'select',
        '#options' => \Drupal::service('plugin.manager.core.layout')
          ->getLayoutOptions(),
        '#default_value' => !empty($item->options['layout']) ? $item->options['layout'] : NULL,
      ];
    }
    if ($entity
      ->bundle() != 'layout') {
      $element['options']['view_mode'] = [
        '#type' => 'select',
        '#options' => \Drupal::service('entity_display.repository')
          ->getViewModeOptionsByBundle($entity
          ->getEntityTypeId(), $entity
          ->bundle()),
        '#default_value' => !empty($item->options['view_mode']) ? $item->options['view_mode'] : NULL,
      ];
    }
  }
  $element['options']['css_class'] = [
    '#type' => 'textfield',
    '#default_value' => !empty($item->options['css_class']) ? $item->options['css_class'] : '',
    '#size' => 10,
    '#attributes' => [
      'placeholder' => t('CSS class(-es)'),
    ],
  ];
}

/* MISC */

/**
 * Prepares variables for `block.html.twig` for `system_powered_by_block`.
 */
function bricks_preprocess_block__system_powered_by_block(&$variables) {
  $bricks_link = '<a href="https://uibricks.com">Bricks</a>';
  $variables['content']['#markup'] = str_replace('>Drupal</a>', '>Drupal</a> & ' . $bricks_link, $variables['content']['#markup']);
}

Functions

Namesort descending Description
bricks_field_widget_bricks_tree_autocomplete_form_alter Implements hook_field_widget_WIDGET_TYPE_form_alter() for `bricks_tree_autocomplete`.
bricks_field_widget_entity_reference_autocomplete_form_alter Implements hook_field_widget_WIDGET_TYPE_form_alter() for `entity_reference_autocomplete`.
bricks_field_widget_info_alter Implements hook_field_widget_info_alter().
bricks_field_widget_paragraphs_form_alter Implements hook_field_widget_WIDGET_TYPE_form_alter() for `paragraphs`.
bricks_preprocess_block__system_powered_by_block Prepares variables for `block.html.twig` for `system_powered_by_block`.
bricks_preprocess_field Prepares variables for `field.html.twig`.
bricks_preprocess_field_multiple_value_form Prepares variables for `field-multiple-value-form.html.twig`.
_bricks_build_layout_from_items Helper function for layout handling in _bricks_nest_items().
_bricks_form_element_alter Helper function for widget's formElement().
_bricks_nested_get_new_elements
_bricks_nest_get_field_item Helper to get the field item list a render array is made from.
_bricks_nest_get_new_element Assemble a render element based on existing content and field item data.
_bricks_nest_items Helper function: converts element's items to a tree structure.
_bricks_preprocess_tabledrag_form Helper function for hook_preprocess_field_multiple_value_form().

Constants

Namesort descending Description
BRICKS_TREE_ROOT When rearranging a list of entity view render arrays into layouts, a list of parents are kept and those parents are nonnegative integers so the root is an index which can't occur otherwise.