diff core/modules/layout_builder/src/QuickEditIntegration.php @ 18:af1871eacc83

Update to Drupal core 8.7.1
author Chris Cannam
date Thu, 09 May 2019 15:33:08 +0100
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/core/modules/layout_builder/src/QuickEditIntegration.php	Thu May 09 15:33:08 2019 +0100
@@ -0,0 +1,318 @@
+<?php
+
+namespace Drupal\layout_builder;
+
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Cache\CacheableMetadata;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Entity\FieldableEntityInterface;
+use Drupal\Core\Logger\LoggerChannelTrait;
+use Drupal\Core\Plugin\Context\Context;
+use Drupal\Core\Plugin\Context\ContextDefinition;
+use Drupal\Core\Plugin\Context\EntityContext;
+use Drupal\Core\Render\Element;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Helper methods for Quick Edit module integration.
+ *
+ * @internal
+ *   This is an internal utility class wrapping hook implementations.
+ */
+class QuickEditIntegration implements ContainerInjectionInterface {
+
+  use LoggerChannelTrait;
+
+  /**
+   * The section storage manager.
+   *
+   * @var \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface
+   */
+  protected $sectionStorageManager;
+
+  /**
+   * The current user.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $currentUser;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Constructs a new QuickEditIntegration object.
+   *
+   * @param \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface $section_storage_manager
+   *   The section storage manager.
+   * @param \Drupal\Core\Session\AccountInterface $current_user
+   *   The current user.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   */
+  public function __construct(SectionStorageManagerInterface $section_storage_manager, AccountInterface $current_user, EntityTypeManagerInterface $entity_type_manager) {
+    $this->sectionStorageManager = $section_storage_manager;
+    $this->currentUser = $current_user;
+    $this->entityTypeManager = $entity_type_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('plugin.manager.layout_builder.section_storage'),
+      $container->get('current_user'),
+      $container->get('entity_type.manager')
+    );
+  }
+
+  /**
+   * Alters the entity view build for Quick Edit compatibility.
+   *
+   * When rendering fields outside of normal view modes, Quick Edit requires
+   * that modules identify themselves with a view mode ID in the format
+   * [module_name]-[information the module needs to rerender], as prescribed by
+   * hook_quickedit_render_field().
+   *
+   * @param array $build
+   *   The built entity render array.
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity.
+   * @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display
+   *   The entity view display.
+   *
+   * @see hook_quickedit_render_field()
+   * @see layout_builder_quickedit_render_field()
+   */
+  public function entityViewAlter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) {
+    if (!$entity instanceof FieldableEntityInterface || !isset($build['_layout_builder'])) {
+      return;
+    }
+
+    $build['#cache']['contexts'][] = 'user.permissions';
+    if (!$this->currentUser->hasPermission('access in-place editing')) {
+      return;
+    }
+
+    $cacheable_metadata = CacheableMetadata::createFromRenderArray($build);
+    $section_list = $this->sectionStorageManager->findByContext(
+      [
+        'display' => EntityContext::fromEntity($display),
+        'entity' => EntityContext::fromEntity($entity),
+        'view_mode' => new Context(new ContextDefinition('string'), $display->getMode()),
+      ],
+      $cacheable_metadata
+    );
+    $cacheable_metadata->applyTo($build);
+
+    if (empty($section_list)) {
+      return;
+    }
+
+    // Create a hash of the sections and use it in the unique Quick Edit view
+    // mode ID. Any changes to the sections will result in a different hash,
+    // forcing Quick Edit's JavaScript to recognize any changes and retrieve
+    // up-to-date metadata.
+    $sections_hash = hash('sha256', serialize($section_list->getSections()));
+
+    // Track each component by their plugin ID, delta, region, and UUID.
+    $plugin_ids_to_update = [];
+    foreach (Element::children($build['_layout_builder']) as $delta) {
+      $section = $build['_layout_builder'][$delta];
+      /** @var \Drupal\Core\Layout\LayoutDefinition $layout */
+      $layout = $section['#layout'];
+      $regions = $layout->getRegionNames();
+
+      foreach ($regions as $region) {
+        if (isset($section[$region])) {
+          foreach ($section[$region] as $uuid => $component) {
+            if (isset($component['#plugin_id']) && $this->supportQuickEditOnComponent($component, $entity)) {
+              $plugin_ids_to_update[$component['#plugin_id']][$delta][$region][$uuid] = $uuid;
+            }
+          }
+        }
+      }
+    }
+
+    // @todo Remove when https://www.drupal.org/node/3041850 is resolved.
+    $plugin_ids_to_update = array_filter($plugin_ids_to_update, function ($info) {
+      // Delta, region, and UUID each count as one.
+      return count($info, COUNT_RECURSIVE) === 3;
+    });
+
+    $plugin_ids_to_update = NestedArray::mergeDeepArray($plugin_ids_to_update, TRUE);
+    foreach ($plugin_ids_to_update as $delta => $regions) {
+      foreach ($regions as $region => $uuids) {
+        foreach ($uuids as $uuid => $component) {
+          $build['_layout_builder'][$delta][$region][$uuid]['content']['#view_mode'] = static::getViewModeId($entity, $display, $delta, $uuid, $sections_hash);
+        }
+      }
+    }
+    // Alter the Quick Edit view mode ID of all fields outside of the Layout
+    // Builder sections to force Quick Edit to request to the field metadata.
+    // @todo Remove this logic in https://www.drupal.org/project/node/2966136.
+    foreach (Element::children($build) as $field_name) {
+      if ($field_name !== '_layout_builder') {
+        $field_build = &$build[$field_name];
+        if (isset($field_build['#view_mode'])) {
+          $field_build['#view_mode'] = "layout_builder-{$display->getMode()}-non_component-$sections_hash";
+        }
+      }
+    }
+  }
+
+  /**
+   * Generates a Quick Edit view mode ID.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity.
+   * @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display
+   *   The entity view display.
+   * @param int $delta
+   *   The delta.
+   * @param string $component_uuid
+   *   The component UUID.
+   * @param string $sections_hash
+   *   The hash of the sections; must change whenever the sections change.
+   *
+   * @return string
+   *   The Quick Edit view mode ID.
+   *
+   * @see \Drupal\layout_builder\QuickEditIntegration::deconstructViewModeId()
+   */
+  private static function getViewModeId(EntityInterface $entity, EntityViewDisplayInterface $display, $delta, $component_uuid, $sections_hash) {
+    return implode('-', [
+      'layout_builder',
+      $display->getMode(),
+      $delta,
+      // Replace the dashes in the component UUID because we need to
+      // use dashes to join the parts.
+      str_replace('-', '_', $component_uuid),
+      $entity->id(),
+      $sections_hash,
+    ]);
+  }
+
+  /**
+   * Deconstructs the Quick Edit view mode ID into its constituent parts.
+   *
+   * @param string $quick_edit_view_mode_id
+   *   The Quick Edit view mode ID.
+   *
+   * @return array
+   *   An array containing the entity view mode ID, the delta, the component
+   *   UUID, and the entity ID.
+   *
+   * @see \Drupal\layout_builder\QuickEditIntegration::getViewModeId()
+   */
+  public static function deconstructViewModeId($quick_edit_view_mode_id) {
+    list(, $entity_view_mode_id, $delta, $component_uuid, $entity_id) = explode('-', $quick_edit_view_mode_id, 7);
+    return [
+      $entity_view_mode_id,
+      // @todo Explicitly cast delta to an integer, remove this in
+      //   https://www.drupal.org/project/drupal/issues/2984509.
+      (int) $delta,
+      // Replace the underscores with dash to get back the component UUID.
+      str_replace('_', '-', $component_uuid),
+      $entity_id,
+    ];
+  }
+
+  /**
+   * Re-renders a field rendered by Layout Builder, edited with Quick Edit.
+   *
+   * @param \Drupal\Core\Entity\FieldableEntityInterface $entity
+   *   The entity.
+   * @param string $field_name
+   *   The field name.
+   * @param string $quick_edit_view_mode_id
+   *   The Quick Edit view mode ID.
+   * @param string $langcode
+   *   The language code.
+   *
+   * @return array
+   *   The re-rendered field.
+   */
+  public function quickEditRenderField(FieldableEntityInterface $entity, $field_name, $quick_edit_view_mode_id, $langcode) {
+    list($entity_view_mode, $delta, $component_uuid) = static::deconstructViewModeId($quick_edit_view_mode_id);
+
+    $entity_build = $this->entityTypeManager->getViewBuilder($entity->getEntityTypeId())->view($entity, $entity_view_mode, $langcode);
+    $this->buildEntityView($entity_build);
+
+    if (isset($entity_build['_layout_builder'][$delta])) {
+      foreach (Element::children($entity_build['_layout_builder'][$delta]) as $region) {
+        if (isset($entity_build['_layout_builder'][$delta][$region][$component_uuid])) {
+          return $entity_build['_layout_builder'][$delta][$region][$component_uuid]['content'];
+        }
+      }
+    }
+
+    $this->getLogger('layout_builder')->warning('The field "%field" failed to render.', ['%field' => $field_name]);
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * @todo Replace this hardcoded processing when
+   *   https://www.drupal.org/project/drupal/issues/3041635 is resolved.
+   *
+   * @see \Drupal\Tests\EntityViewTrait::buildEntityView()
+   */
+  private function buildEntityView(array &$elements) {
+    // If the default values for this element have not been loaded yet,
+    // populate them.
+    if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) {
+      $elements += \Drupal::service('element_info')->getInfo($elements['#type']);
+    }
+
+    // Make any final changes to the element before it is rendered. This means
+    // that the $element or the children can be altered or corrected before
+    // the element is rendered into the final text.
+    if (isset($elements['#pre_render'])) {
+      foreach ($elements['#pre_render'] as $callable) {
+        $elements = call_user_func($callable, $elements);
+      }
+    }
+
+    // And recurse.
+    $children = Element::children($elements, TRUE);
+    foreach ($children as $key) {
+      $this->buildEntityView($elements[$key]);
+    }
+  }
+
+  /**
+   * Determines whether a component has Quick Edit support.
+   *
+   * Only field_block components for display configurable fields should be
+   * supported.
+   *
+   * @param array $component
+   *   The component render array.
+   * @param \Drupal\Core\Entity\FieldableEntityInterface $entity
+   *   The entity being displayed.
+   *
+   * @return bool
+   *   Whether Quick Edit is supported on the component.
+   *
+   * @see \Drupal\layout_builder\Plugin\Block\FieldBlock
+   */
+  private function supportQuickEditOnComponent(array $component, FieldableEntityInterface $entity) {
+    if (isset($component['content']['#field_name'], $component['#base_plugin_id']) && $component['#base_plugin_id'] === 'field_block' && $entity->hasField($component['content']['#field_name'])) {
+      return $entity->getFieldDefinition($component['content']['#field_name'])->isDisplayConfigurable('view');
+    }
+    return FALSE;
+  }
+
+}