Chris@14: 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@14: return $this; Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ 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@14: $this->setThirdPartySetting('layout_builder', 'sections', array_values($sections)); 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@14: $this->addSectionField($entity_type_id, $bundle, 'layout_builder__layout'); Chris@14: } Chris@14: elseif ($field = FieldConfig::loadByName($entity_type_id, $bundle, 'layout_builder__layout')) { Chris@14: $field->delete(); Chris@14: } Chris@14: } 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@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@14: $field->save(); Chris@14: } Chris@14: } Chris@14: Chris@14: /** Chris@14: * {@inheritdoc} Chris@14: */ 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@14: $sections = $this->getRuntimeSections($entity); Chris@14: if ($sections) { 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: // Bypass ::getContexts() in order to use the runtime entity, not a Chris@14: // sample entity. Chris@14: $contexts = $this->contextRepository()->getAvailableContexts(); Chris@14: // @todo Use EntityContextDefinition after resolving Chris@14: // https://www.drupal.org/node/2932462. Chris@14: $contexts['layout_builder.entity'] = new Context(new ContextDefinition("entity:{$entity->getEntityTypeId()}", new TranslatableMarkup('@entity being viewed', ['@entity' => $entity->getEntityType()->getLabel()])), $entity); Chris@14: foreach ($sections as $delta => $section) { Chris@14: $build_list[$id]['_layout_builder'][$delta] = $section->toRenderArray($contexts); Chris@14: } Chris@14: } Chris@14: } Chris@14: Chris@14: return $build_list; Chris@14: } Chris@14: Chris@14: /** 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@14: */ Chris@14: protected function getRuntimeSections(FieldableEntityInterface $entity) { Chris@14: if ($this->isOverridable() && !$entity->get('layout_builder__layout')->isEmpty()) { Chris@14: return $entity->get('layout_builder__layout')->getSections(); Chris@14: } Chris@14: Chris@14: return $this->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: * Calculates and returns dependencies of a specific plugin instance. Chris@14: * Chris@14: * @param \Drupal\Component\Plugin\PluginInspectionInterface $instance Chris@14: * The plugin instance. Chris@14: * Chris@14: * @return array Chris@14: * An array of dependencies keyed by the type of dependency. Chris@14: * Chris@14: * @todo Replace this in https://www.drupal.org/project/drupal/issues/2939925. Chris@14: */ Chris@14: protected function getPluginDependencies(PluginInspectionInterface $instance) { Chris@14: $dependencies = []; Chris@14: $definition = $instance->getPluginDefinition(); Chris@14: if ($definition instanceof PluginDefinitionInterface) { Chris@14: $dependencies['module'][] = $definition->getProvider(); Chris@14: if ($definition instanceof DependentPluginDefinitionInterface && $config_dependencies = $definition->getConfigDependencies()) { Chris@14: $dependencies = NestedArray::mergeDeep($dependencies, $config_dependencies); Chris@14: } Chris@14: } Chris@14: elseif (is_array($definition)) { Chris@14: $dependencies['module'][] = $definition['provider']; Chris@14: // Plugins can declare additional dependencies in their definition. Chris@14: if (isset($definition['config_dependencies'])) { Chris@14: $dependencies = NestedArray::mergeDeep($dependencies, $definition['config_dependencies']); Chris@14: } Chris@14: } Chris@14: Chris@14: // If a plugin is dependent, calculate its dependencies. Chris@14: if ($instance instanceof DependentPluginInterface && $plugin_dependencies = $instance->calculateDependencies()) { Chris@14: $dependencies = NestedArray::mergeDeep($dependencies, $plugin_dependencies); Chris@14: } Chris@14: return $dependencies; 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@14: // @todo Remove workaround for EntityViewBuilder::getSingleFieldDisplay() in Chris@14: // https://www.drupal.org/project/drupal/issues/2936464. Chris@14: if ($this->getMode() === static::CUSTOM_MODE) { 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@14: if ($field_definition && $field_definition->isDisplayConfigurable('view') && isset($options['type'])) { Chris@14: $configuration = []; Chris@14: $configuration['id'] = 'field_block:' . $this->getTargetEntityTypeId() . ':' . $this->getTargetBundle() . ':' . $name; Chris@14: $configuration['label_display'] = FALSE; Chris@14: $keys = array_flip(['type', 'label', 'settings', 'third_party_settings']); Chris@14: $configuration['formatter'] = array_intersect_key($options, $keys); Chris@14: $configuration['context_mapping']['entity'] = 'layout_builder.entity'; 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@14: }