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