Chris@14: entityFieldManager = \Drupal::service('entity_field.manager'); Chris@17: parent::__construct($values, $entity_type); Chris@17: } Chris@17: Chris@17: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function isOverridable() { Chris@18: return $this->isLayoutBuilderEnabled() && $this->getThirdPartySetting('layout_builder', 'allow_custom', FALSE); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function setOverridable($overridable = TRUE) { Chris@14: $this->setThirdPartySetting('layout_builder', 'allow_custom', $overridable); Chris@18: // Enable Layout Builder if it's not already enabled and overriding. Chris@18: if ($overridable && !$this->isLayoutBuilderEnabled()) { Chris@18: $this->enableLayoutBuilder(); Chris@18: } Chris@14: return $this; Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@17: public function isLayoutBuilderEnabled() { Chris@18: // To prevent infinite recursion, Layout Builder must not be enabled for the Chris@18: // '_custom' view mode that is used for on-the-fly rendering of fields in Chris@18: // isolation from the entity. Chris@18: if ($this->getOriginalMode() === static::CUSTOM_MODE) { Chris@18: return FALSE; Chris@18: } Chris@17: return (bool) $this->getThirdPartySetting('layout_builder', 'enabled'); Chris@17: } Chris@17: Chris@17: /** Chris@17: * {@inheritdoc} Chris@17: */ Chris@17: public function enableLayoutBuilder() { Chris@17: $this->setThirdPartySetting('layout_builder', 'enabled', TRUE); Chris@17: return $this; Chris@17: } Chris@17: Chris@17: /** Chris@17: * {@inheritdoc} Chris@17: */ Chris@17: public function disableLayoutBuilder() { Chris@17: $this->setOverridable(FALSE); Chris@17: $this->setThirdPartySetting('layout_builder', 'enabled', FALSE); Chris@17: return $this; Chris@17: } Chris@17: Chris@17: /** Chris@17: * {@inheritdoc} Chris@17: */ Chris@14: public function getSections() { Chris@14: return $this->getThirdPartySetting('layout_builder', 'sections', []); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: protected function setSections(array $sections) { Chris@18: // Third-party settings must be completely unset instead of stored as an Chris@18: // empty array. Chris@18: if (!$sections) { Chris@18: $this->unsetThirdPartySetting('layout_builder', 'sections'); Chris@18: } Chris@18: else { Chris@18: $this->setThirdPartySetting('layout_builder', 'sections', array_values($sections)); Chris@18: } Chris@14: return $this; Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function preSave(EntityStorageInterface $storage) { Chris@14: parent::preSave($storage); Chris@14: Chris@14: $original_value = isset($this->original) ? $this->original->isOverridable() : FALSE; Chris@14: $new_value = $this->isOverridable(); Chris@14: if ($original_value !== $new_value) { Chris@14: $entity_type_id = $this->getTargetEntityTypeId(); Chris@14: $bundle = $this->getTargetBundle(); Chris@14: Chris@14: if ($new_value) { Chris@17: $this->addSectionField($entity_type_id, $bundle, OverridesSectionStorage::FIELD_NAME); Chris@14: } Chris@17: else { Chris@17: $this->removeSectionField($entity_type_id, $bundle, OverridesSectionStorage::FIELD_NAME); Chris@14: } Chris@14: } Chris@17: Chris@17: $already_enabled = isset($this->original) ? $this->original->isLayoutBuilderEnabled() : FALSE; Chris@17: $set_enabled = $this->isLayoutBuilderEnabled(); Chris@17: if ($already_enabled !== $set_enabled) { Chris@17: if ($set_enabled) { Chris@17: // Loop through all existing field-based components and add them as Chris@17: // section-based components. Chris@17: $components = $this->getComponents(); Chris@17: // Sort the components by weight. Chris@17: uasort($components, 'Drupal\Component\Utility\SortArray::sortByWeightElement'); Chris@17: foreach ($components as $name => $component) { Chris@17: $this->setComponent($name, $component); Chris@17: } Chris@17: } Chris@17: else { Chris@17: // When being disabled, remove all existing section data. Chris@18: $this->removeAllSections(); Chris@17: } Chris@17: } Chris@17: } Chris@17: Chris@17: /** Chris@17: * Removes a layout section field if it is no longer needed. Chris@17: * Chris@17: * Because the field is shared across all view modes, the field will only be Chris@17: * removed if no other view modes are using it. Chris@17: * Chris@17: * @param string $entity_type_id Chris@17: * The entity type ID. Chris@17: * @param string $bundle Chris@17: * The bundle. Chris@17: * @param string $field_name Chris@17: * The name for the layout section field. Chris@17: */ Chris@17: protected function removeSectionField($entity_type_id, $bundle, $field_name) { Chris@17: $query = $this->entityTypeManager()->getStorage($this->getEntityTypeId())->getQuery() Chris@17: ->condition('targetEntityType', $this->getTargetEntityTypeId()) Chris@17: ->condition('bundle', $this->getTargetBundle()) Chris@17: ->condition('mode', $this->getMode(), '<>') Chris@17: ->condition('third_party_settings.layout_builder.allow_custom', TRUE); Chris@17: $enabled = (bool) $query->count()->execute(); Chris@17: if (!$enabled && $field = FieldConfig::loadByName($entity_type_id, $bundle, $field_name)) { Chris@17: $field->delete(); Chris@17: } Chris@14: } Chris@14: Chris@14: /** Chris@14: * Adds a layout section field to a given bundle. Chris@14: * Chris@14: * @param string $entity_type_id Chris@14: * The entity type ID. Chris@14: * @param string $bundle Chris@14: * The bundle. Chris@14: * @param string $field_name Chris@14: * The name for the layout section field. Chris@14: */ Chris@14: protected function addSectionField($entity_type_id, $bundle, $field_name) { Chris@14: $field = FieldConfig::loadByName($entity_type_id, $bundle, $field_name); Chris@14: if (!$field) { Chris@14: $field_storage = FieldStorageConfig::loadByName($entity_type_id, $field_name); Chris@14: if (!$field_storage) { Chris@14: $field_storage = FieldStorageConfig::create([ Chris@14: 'entity_type' => $entity_type_id, Chris@14: 'field_name' => $field_name, Chris@14: 'type' => 'layout_section', Chris@14: 'locked' => TRUE, Chris@14: ]); Chris@18: $field_storage->setTranslatable(FALSE); Chris@14: $field_storage->save(); Chris@14: } Chris@14: Chris@14: $field = FieldConfig::create([ Chris@14: 'field_storage' => $field_storage, Chris@14: 'bundle' => $bundle, Chris@14: 'label' => t('Layout'), Chris@14: ]); Chris@18: $field->setTranslatable(FALSE); Chris@14: $field->save(); Chris@14: } Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@17: public function createCopy($mode) { Chris@17: // Disable Layout Builder and remove any sections copied from the original. Chris@17: return parent::createCopy($mode) Chris@17: ->setSections([]) Chris@17: ->disableLayoutBuilder(); Chris@17: } Chris@17: Chris@17: /** Chris@17: * {@inheritdoc} Chris@17: */ Chris@14: protected function getDefaultRegion() { Chris@14: if ($this->hasSection(0)) { Chris@14: return $this->getSection(0)->getDefaultRegion(); Chris@14: } Chris@14: Chris@14: return parent::getDefaultRegion(); Chris@14: } Chris@14: Chris@14: /** Chris@14: * Wraps the context repository service. Chris@14: * Chris@14: * @return \Drupal\Core\Plugin\Context\ContextRepositoryInterface Chris@14: * The context repository service. Chris@14: */ Chris@14: protected function contextRepository() { Chris@14: return \Drupal::service('context.repository'); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function buildMultiple(array $entities) { Chris@14: $build_list = parent::buildMultiple($entities); Chris@14: Chris@14: foreach ($entities as $id => $entity) { Chris@18: $build_list[$id]['_layout_builder'] = $this->buildSections($entity); Chris@18: Chris@18: // If there are any sections, remove all fields with configurable display Chris@18: // from the existing build. These fields are replicated within sections as Chris@18: // field blocks by ::setComponent(). Chris@18: if (!Element::isEmpty($build_list[$id]['_layout_builder'])) { Chris@14: foreach ($build_list[$id] as $name => $build_part) { Chris@14: $field_definition = $this->getFieldDefinition($name); Chris@14: if ($field_definition && $field_definition->isDisplayConfigurable($this->displayContext)) { Chris@14: unset($build_list[$id][$name]); Chris@14: } Chris@14: } Chris@14: } Chris@14: } Chris@14: Chris@14: return $build_list; Chris@14: } Chris@14: Chris@14: /** Chris@18: * Builds the render array for the sections of a given entity. Chris@18: * Chris@18: * @param \Drupal\Core\Entity\FieldableEntityInterface $entity Chris@18: * The entity. Chris@18: * Chris@18: * @return array Chris@18: * The render array representing the sections of the entity. Chris@18: */ Chris@18: protected function buildSections(FieldableEntityInterface $entity) { Chris@18: $contexts = $this->getContextsForEntity($entity); Chris@18: // @todo Remove in https://www.drupal.org/project/drupal/issues/3018782. Chris@18: $label = new TranslatableMarkup('@entity being viewed', [ Chris@18: '@entity' => $entity->getEntityType()->getSingularLabel(), Chris@18: ]); Chris@18: $contexts['layout_builder.entity'] = EntityContext::fromEntity($entity, $label); Chris@18: Chris@18: $cacheability = new CacheableMetadata(); Chris@18: $storage = $this->sectionStorageManager()->findByContext($contexts, $cacheability); Chris@18: Chris@18: $build = []; Chris@18: if ($storage) { Chris@18: foreach ($storage->getSections() as $delta => $section) { Chris@18: $build[$delta] = $section->toRenderArray($contexts); Chris@18: } Chris@18: } Chris@18: // The render array is built based on decisions made by @SectionStorage Chris@18: // plugins and therefore it needs to depend on the accumulated Chris@18: // cacheability of those decisions. Chris@18: $cacheability->applyTo($build); Chris@18: return $build; Chris@18: } Chris@18: Chris@18: /** Chris@18: * Gets the available contexts for a given entity. Chris@18: * Chris@18: * @param \Drupal\Core\Entity\FieldableEntityInterface $entity Chris@18: * The entity. Chris@18: * Chris@18: * @return \Drupal\Core\Plugin\Context\ContextInterface[] Chris@18: * An array of context objects for a given entity. Chris@18: */ Chris@18: protected function getContextsForEntity(FieldableEntityInterface $entity) { Chris@18: return [ Chris@18: 'view_mode' => new Context(ContextDefinition::create('string'), $this->getMode()), Chris@18: 'entity' => EntityContext::fromEntity($entity), Chris@18: 'display' => EntityContext::fromEntity($this), Chris@18: ] + $this->contextRepository()->getAvailableContexts(); Chris@18: } Chris@18: Chris@18: /** Chris@14: * Gets the runtime sections for a given entity. Chris@14: * Chris@14: * @param \Drupal\Core\Entity\FieldableEntityInterface $entity Chris@14: * The entity. Chris@14: * Chris@14: * @return \Drupal\layout_builder\Section[] Chris@14: * The sections. Chris@18: * Chris@18: * @deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. Chris@18: * \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface::findByContext() Chris@18: * should be used instead. See https://www.drupal.org/node/3022574. Chris@14: */ Chris@14: protected function getRuntimeSections(FieldableEntityInterface $entity) { Chris@18: @trigger_error('\Drupal\layout_builder\Entity\LayoutBuilderEntityViewDisplay::getRuntimeSections() is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface::findByContext() should be used instead. See https://www.drupal.org/node/3022574.', E_USER_DEPRECATED); Chris@18: // For backwards compatibility, mimic the functionality of ::buildSections() Chris@18: // by constructing a cacheable metadata object and retrieving the Chris@18: // entity-based contexts. Chris@18: $cacheability = new CacheableMetadata(); Chris@18: $contexts = $this->getContextsForEntity($entity); Chris@18: $storage = $this->sectionStorageManager()->findByContext($contexts, $cacheability); Chris@18: return $storage ? $storage->getSections() : []; Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: * Chris@14: * @todo Move this upstream in https://www.drupal.org/node/2939931. Chris@14: */ Chris@14: public function label() { Chris@14: $bundle_info = \Drupal::service('entity_type.bundle.info')->getBundleInfo($this->getTargetEntityTypeId()); Chris@14: $bundle_label = $bundle_info[$this->getTargetBundle()]['label']; Chris@14: $target_entity_type = $this->entityTypeManager()->getDefinition($this->getTargetEntityTypeId()); Chris@14: return new TranslatableMarkup('@bundle @label', ['@bundle' => $bundle_label, '@label' => $target_entity_type->getPluralLabel()]); Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function calculateDependencies() { Chris@14: parent::calculateDependencies(); Chris@14: Chris@14: foreach ($this->getSections() as $delta => $section) { Chris@14: $this->calculatePluginDependencies($section->getLayout()); Chris@14: foreach ($section->getComponents() as $uuid => $component) { Chris@14: $this->calculatePluginDependencies($component->getPlugin()); Chris@14: } Chris@14: } Chris@14: Chris@14: return $this; Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function onDependencyRemoval(array $dependencies) { Chris@14: $changed = parent::onDependencyRemoval($dependencies); Chris@14: Chris@14: // Loop through all sections and determine if the removed dependencies are Chris@14: // used by their layout plugins. Chris@14: foreach ($this->getSections() as $delta => $section) { Chris@14: $layout_dependencies = $this->getPluginDependencies($section->getLayout()); Chris@14: $layout_removed_dependencies = $this->getPluginRemovedDependencies($layout_dependencies, $dependencies); Chris@14: if ($layout_removed_dependencies) { Chris@14: // @todo Allow the plugins to react to their dependency removal in Chris@14: // https://www.drupal.org/project/drupal/issues/2579743. Chris@14: $this->removeSection($delta); Chris@14: $changed = TRUE; Chris@14: } Chris@14: // If the section is not removed, loop through all components. Chris@14: else { Chris@14: foreach ($section->getComponents() as $uuid => $component) { Chris@14: $plugin_dependencies = $this->getPluginDependencies($component->getPlugin()); Chris@14: $component_removed_dependencies = $this->getPluginRemovedDependencies($plugin_dependencies, $dependencies); Chris@14: if ($component_removed_dependencies) { Chris@14: // @todo Allow the plugins to react to their dependency removal in Chris@14: // https://www.drupal.org/project/drupal/issues/2579743. Chris@14: $section->removeComponent($uuid); Chris@14: $changed = TRUE; Chris@14: } Chris@14: } Chris@14: } Chris@14: } Chris@14: return $changed; Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ Chris@14: public function setComponent($name, array $options = []) { Chris@14: parent::setComponent($name, $options); Chris@14: Chris@17: // Only continue if Layout Builder is enabled. Chris@17: if (!$this->isLayoutBuilderEnabled()) { Chris@14: return $this; Chris@14: } Chris@14: Chris@14: // Retrieve the updated options after the parent:: call. Chris@14: $options = $this->content[$name]; Chris@14: // Provide backwards compatibility by converting to a section component. Chris@14: $field_definition = $this->getFieldDefinition($name); Chris@17: $extra_fields = $this->entityFieldManager->getExtraFields($this->getTargetEntityTypeId(), $this->getTargetBundle()); Chris@17: $is_view_configurable_non_extra_field = $field_definition && $field_definition->isDisplayConfigurable('view') && isset($options['type']); Chris@17: if ($is_view_configurable_non_extra_field || isset($extra_fields['display'][$name])) { Chris@17: $configuration = [ Chris@17: 'label_display' => '0', Chris@17: 'context_mapping' => ['entity' => 'layout_builder.entity'], Chris@17: ]; Chris@17: if ($is_view_configurable_non_extra_field) { Chris@17: $configuration['id'] = 'field_block:' . $this->getTargetEntityTypeId() . ':' . $this->getTargetBundle() . ':' . $name; Chris@17: $keys = array_flip(['type', 'label', 'settings', 'third_party_settings']); Chris@17: $configuration['formatter'] = array_intersect_key($options, $keys); Chris@17: } Chris@17: else { Chris@17: $configuration['id'] = 'extra_field_block:' . $this->getTargetEntityTypeId() . ':' . $this->getTargetBundle() . ':' . $name; Chris@17: } Chris@14: Chris@14: $section = $this->getDefaultSection(); Chris@14: $region = isset($options['region']) ? $options['region'] : $section->getDefaultRegion(); Chris@14: $new_component = (new SectionComponent(\Drupal::service('uuid')->generate(), $region, $configuration)); Chris@14: $section->appendComponent($new_component); Chris@14: } Chris@14: return $this; Chris@14: } Chris@14: Chris@14: /** Chris@14: * Gets a default section. Chris@14: * Chris@14: * @return \Drupal\layout_builder\Section Chris@14: * The default section. Chris@14: */ Chris@14: protected function getDefaultSection() { Chris@14: // If no section exists, append a new one. Chris@14: if (!$this->hasSection(0)) { Chris@14: $this->appendSection(new Section('layout_onecol')); Chris@14: } Chris@14: Chris@14: // Return the first section. Chris@14: return $this->getSection(0); Chris@14: } Chris@14: Chris@18: /** Chris@18: * Gets the section storage manager. Chris@18: * Chris@18: * @return \Drupal\layout_builder\SectionStorage\SectionStorageManagerInterface Chris@18: * The section storage manager. Chris@18: */ Chris@18: private function sectionStorageManager() { Chris@18: return \Drupal::service('plugin.manager.layout_builder.section_storage'); Chris@18: } Chris@18: Chris@18: /** Chris@18: * {@inheritdoc} Chris@18: */ Chris@18: public function getComponent($name) { Chris@18: if ($this->isLayoutBuilderEnabled() && $section_component = $this->getQuickEditSectionComponent() ?: $this->getSectionComponentForFieldName($name)) { Chris@18: $plugin = $section_component->getPlugin(); Chris@18: if ($plugin instanceof ConfigurableInterface) { Chris@18: $configuration = $plugin->getConfiguration(); Chris@18: if (isset($configuration['formatter'])) { Chris@18: return $configuration['formatter']; Chris@18: } Chris@18: } Chris@18: } Chris@18: return parent::getComponent($name); Chris@18: } Chris@18: Chris@18: /** Chris@18: * Returns the Quick Edit formatter settings. Chris@18: * Chris@18: * @return \Drupal\layout_builder\SectionComponent|null Chris@18: * The section component if it is available. Chris@18: * Chris@18: * @see \Drupal\layout_builder\QuickEditIntegration::entityViewAlter() Chris@18: * @see \Drupal\quickedit\MetadataGenerator::generateFieldMetadata() Chris@18: */ Chris@18: private function getQuickEditSectionComponent() { Chris@18: // To determine the Quick Edit view_mode ID we need an originalMode set. Chris@18: if ($original_mode = $this->getOriginalMode()) { Chris@18: $parts = explode('-', $original_mode); Chris@18: // The Quick Edit view mode ID is created by Chris@18: // \Drupal\layout_builder\QuickEditIntegration::entityViewAlter() Chris@18: // concatenating together the information we need to retrieve the Layout Chris@18: // Builder component. It follows the structure prescribed by the Chris@18: // documentation of hook_quickedit_render_field(). Chris@18: if (count($parts) === 6 && $parts[0] === 'layout_builder') { Chris@18: list(, $delta, $component_uuid, $entity_id) = QuickEditIntegration::deconstructViewModeId($original_mode); Chris@18: $entity = $this->entityTypeManager()->getStorage($this->getTargetEntityTypeId())->load($entity_id); Chris@18: $sections = $this->getEntitySections($entity); Chris@18: if (isset($sections[$delta])) { Chris@18: $component = $sections[$delta]->getComponent($component_uuid); Chris@18: $plugin = $component->getPlugin(); Chris@18: // We only care about FieldBlock because these are only components Chris@18: // that provide Quick Edit integration: Quick Edit enables in-place Chris@18: // editing of fields of entities, not of anything else. Chris@18: if ($plugin instanceof DerivativeInspectionInterface && $plugin->getBaseId() === 'field_block') { Chris@18: return $component; Chris@18: } Chris@18: } Chris@18: } Chris@18: } Chris@18: return NULL; Chris@18: } Chris@18: Chris@18: /** Chris@18: * Gets the component for a given field name if any. Chris@18: * Chris@18: * @param string $field_name Chris@18: * The field name. Chris@18: * Chris@18: * @return \Drupal\layout_builder\SectionComponent|null Chris@18: * The section component if it is available. Chris@18: */ Chris@18: private function getSectionComponentForFieldName($field_name) { Chris@18: // Loop through every component until the first match is found. Chris@18: foreach ($this->getSections() as $section) { Chris@18: foreach ($section->getComponents() as $component) { Chris@18: $plugin = $component->getPlugin(); Chris@18: if ($plugin instanceof DerivativeInspectionInterface && $plugin->getBaseId() === 'field_block') { Chris@18: // FieldBlock derivative IDs are in the format Chris@18: // [entity_type]:[bundle]:[field]. Chris@18: list(, , $field_block_field_name) = explode(PluginBase::DERIVATIVE_SEPARATOR, $plugin->getDerivativeId()); Chris@18: if ($field_block_field_name === $field_name) { Chris@18: return $component; Chris@18: } Chris@18: } Chris@18: } Chris@18: } Chris@18: return NULL; Chris@18: } Chris@18: Chris@14: }