3 namespace Drupal\layout_builder\Entity;
5 use Drupal\Core\Entity\Entity\EntityViewDisplay as BaseEntityViewDisplay;
6 use Drupal\Core\Entity\EntityStorageInterface;
7 use Drupal\Core\Entity\FieldableEntityInterface;
8 use Drupal\Core\Plugin\Context\EntityContext;
9 use Drupal\Core\StringTranslation\TranslatableMarkup;
10 use Drupal\field\Entity\FieldConfig;
11 use Drupal\field\Entity\FieldStorageConfig;
12 use Drupal\layout_builder\Plugin\SectionStorage\OverridesSectionStorage;
13 use Drupal\layout_builder\Section;
14 use Drupal\layout_builder\SectionComponent;
15 use Drupal\layout_builder\SectionStorage\SectionStorageTrait;
18 * Provides an entity view display entity that has a layout.
21 * Layout Builder is currently experimental and should only be leveraged by
22 * experimental modules and development releases of contributed modules.
23 * See https://www.drupal.org/core/experimental for more information.
25 class LayoutBuilderEntityViewDisplay extends BaseEntityViewDisplay implements LayoutEntityDisplayInterface {
27 use SectionStorageTrait;
30 * The entity field manager.
32 * @var \Drupal\Core\Entity\EntityFieldManagerInterface
34 protected $entityFieldManager;
39 public function __construct(array $values, $entity_type) {
40 // Set $entityFieldManager before calling the parent constructor because the
41 // constructor will call init() which then calls setComponent() which needs
42 // $entityFieldManager.
43 $this->entityFieldManager = \Drupal::service('entity_field.manager');
44 parent::__construct($values, $entity_type);
50 public function isOverridable() {
51 return $this->getThirdPartySetting('layout_builder', 'allow_custom', FALSE);
57 public function setOverridable($overridable = TRUE) {
58 $this->setThirdPartySetting('layout_builder', 'allow_custom', $overridable);
65 public function isLayoutBuilderEnabled() {
66 return (bool) $this->getThirdPartySetting('layout_builder', 'enabled');
72 public function enableLayoutBuilder() {
73 $this->setThirdPartySetting('layout_builder', 'enabled', TRUE);
80 public function disableLayoutBuilder() {
81 $this->setOverridable(FALSE);
82 $this->setThirdPartySetting('layout_builder', 'enabled', FALSE);
89 public function getSections() {
90 return $this->getThirdPartySetting('layout_builder', 'sections', []);
96 protected function setSections(array $sections) {
97 $this->setThirdPartySetting('layout_builder', 'sections', array_values($sections));
104 public function preSave(EntityStorageInterface $storage) {
105 parent::preSave($storage);
107 $original_value = isset($this->original) ? $this->original->isOverridable() : FALSE;
108 $new_value = $this->isOverridable();
109 if ($original_value !== $new_value) {
110 $entity_type_id = $this->getTargetEntityTypeId();
111 $bundle = $this->getTargetBundle();
114 $this->addSectionField($entity_type_id, $bundle, OverridesSectionStorage::FIELD_NAME);
117 $this->removeSectionField($entity_type_id, $bundle, OverridesSectionStorage::FIELD_NAME);
121 $already_enabled = isset($this->original) ? $this->original->isLayoutBuilderEnabled() : FALSE;
122 $set_enabled = $this->isLayoutBuilderEnabled();
123 if ($already_enabled !== $set_enabled) {
125 // Loop through all existing field-based components and add them as
126 // section-based components.
127 $components = $this->getComponents();
128 // Sort the components by weight.
129 uasort($components, 'Drupal\Component\Utility\SortArray::sortByWeightElement');
130 foreach ($components as $name => $component) {
131 $this->setComponent($name, $component);
135 // When being disabled, remove all existing section data.
136 while (count($this) > 0) {
137 $this->removeSection(0);
144 * Removes a layout section field if it is no longer needed.
146 * Because the field is shared across all view modes, the field will only be
147 * removed if no other view modes are using it.
149 * @param string $entity_type_id
150 * The entity type ID.
151 * @param string $bundle
153 * @param string $field_name
154 * The name for the layout section field.
156 protected function removeSectionField($entity_type_id, $bundle, $field_name) {
157 $query = $this->entityTypeManager()->getStorage($this->getEntityTypeId())->getQuery()
158 ->condition('targetEntityType', $this->getTargetEntityTypeId())
159 ->condition('bundle', $this->getTargetBundle())
160 ->condition('mode', $this->getMode(), '<>')
161 ->condition('third_party_settings.layout_builder.allow_custom', TRUE);
162 $enabled = (bool) $query->count()->execute();
163 if (!$enabled && $field = FieldConfig::loadByName($entity_type_id, $bundle, $field_name)) {
169 * Adds a layout section field to a given bundle.
171 * @param string $entity_type_id
172 * The entity type ID.
173 * @param string $bundle
175 * @param string $field_name
176 * The name for the layout section field.
178 protected function addSectionField($entity_type_id, $bundle, $field_name) {
179 $field = FieldConfig::loadByName($entity_type_id, $bundle, $field_name);
181 $field_storage = FieldStorageConfig::loadByName($entity_type_id, $field_name);
182 if (!$field_storage) {
183 $field_storage = FieldStorageConfig::create([
184 'entity_type' => $entity_type_id,
185 'field_name' => $field_name,
186 'type' => 'layout_section',
189 $field_storage->save();
192 $field = FieldConfig::create([
193 'field_storage' => $field_storage,
195 'label' => t('Layout'),
204 public function createCopy($mode) {
205 // Disable Layout Builder and remove any sections copied from the original.
206 return parent::createCopy($mode)
208 ->disableLayoutBuilder();
214 protected function getDefaultRegion() {
215 if ($this->hasSection(0)) {
216 return $this->getSection(0)->getDefaultRegion();
219 return parent::getDefaultRegion();
223 * Wraps the context repository service.
225 * @return \Drupal\Core\Plugin\Context\ContextRepositoryInterface
226 * The context repository service.
228 protected function contextRepository() {
229 return \Drupal::service('context.repository');
235 public function buildMultiple(array $entities) {
236 $build_list = parent::buildMultiple($entities);
237 if (!$this->isLayoutBuilderEnabled()) {
241 /** @var \Drupal\Core\Entity\EntityInterface $entity */
242 foreach ($entities as $id => $entity) {
243 $sections = $this->getRuntimeSections($entity);
245 foreach ($build_list[$id] as $name => $build_part) {
246 $field_definition = $this->getFieldDefinition($name);
247 if ($field_definition && $field_definition->isDisplayConfigurable($this->displayContext)) {
248 unset($build_list[$id][$name]);
252 // Bypass ::getContexts() in order to use the runtime entity, not a
254 $contexts = $this->contextRepository()->getAvailableContexts();
255 $label = new TranslatableMarkup('@entity being viewed', [
256 '@entity' => $entity->getEntityType()->getSingularLabel(),
258 $contexts['layout_builder.entity'] = EntityContext::fromEntity($entity, $label);
259 foreach ($sections as $delta => $section) {
260 $build_list[$id]['_layout_builder'][$delta] = $section->toRenderArray($contexts);
269 * Gets the runtime sections for a given entity.
271 * @param \Drupal\Core\Entity\FieldableEntityInterface $entity
274 * @return \Drupal\layout_builder\Section[]
277 protected function getRuntimeSections(FieldableEntityInterface $entity) {
278 if ($this->isOverridable() && !$entity->get(OverridesSectionStorage::FIELD_NAME)->isEmpty()) {
279 return $entity->get(OverridesSectionStorage::FIELD_NAME)->getSections();
282 return $this->getSections();
288 * @todo Move this upstream in https://www.drupal.org/node/2939931.
290 public function label() {
291 $bundle_info = \Drupal::service('entity_type.bundle.info')->getBundleInfo($this->getTargetEntityTypeId());
292 $bundle_label = $bundle_info[$this->getTargetBundle()]['label'];
293 $target_entity_type = $this->entityTypeManager()->getDefinition($this->getTargetEntityTypeId());
294 return new TranslatableMarkup('@bundle @label', ['@bundle' => $bundle_label, '@label' => $target_entity_type->getPluralLabel()]);
300 public function calculateDependencies() {
301 parent::calculateDependencies();
303 foreach ($this->getSections() as $delta => $section) {
304 $this->calculatePluginDependencies($section->getLayout());
305 foreach ($section->getComponents() as $uuid => $component) {
306 $this->calculatePluginDependencies($component->getPlugin());
316 public function onDependencyRemoval(array $dependencies) {
317 $changed = parent::onDependencyRemoval($dependencies);
319 // Loop through all sections and determine if the removed dependencies are
320 // used by their layout plugins.
321 foreach ($this->getSections() as $delta => $section) {
322 $layout_dependencies = $this->getPluginDependencies($section->getLayout());
323 $layout_removed_dependencies = $this->getPluginRemovedDependencies($layout_dependencies, $dependencies);
324 if ($layout_removed_dependencies) {
325 // @todo Allow the plugins to react to their dependency removal in
326 // https://www.drupal.org/project/drupal/issues/2579743.
327 $this->removeSection($delta);
330 // If the section is not removed, loop through all components.
332 foreach ($section->getComponents() as $uuid => $component) {
333 $plugin_dependencies = $this->getPluginDependencies($component->getPlugin());
334 $component_removed_dependencies = $this->getPluginRemovedDependencies($plugin_dependencies, $dependencies);
335 if ($component_removed_dependencies) {
336 // @todo Allow the plugins to react to their dependency removal in
337 // https://www.drupal.org/project/drupal/issues/2579743.
338 $section->removeComponent($uuid);
350 public function setComponent($name, array $options = []) {
351 parent::setComponent($name, $options);
353 // Only continue if Layout Builder is enabled.
354 if (!$this->isLayoutBuilderEnabled()) {
358 // Retrieve the updated options after the parent:: call.
359 $options = $this->content[$name];
360 // Provide backwards compatibility by converting to a section component.
361 $field_definition = $this->getFieldDefinition($name);
362 $extra_fields = $this->entityFieldManager->getExtraFields($this->getTargetEntityTypeId(), $this->getTargetBundle());
363 $is_view_configurable_non_extra_field = $field_definition && $field_definition->isDisplayConfigurable('view') && isset($options['type']);
364 if ($is_view_configurable_non_extra_field || isset($extra_fields['display'][$name])) {
366 'label_display' => '0',
367 'context_mapping' => ['entity' => 'layout_builder.entity'],
369 if ($is_view_configurable_non_extra_field) {
370 $configuration['id'] = 'field_block:' . $this->getTargetEntityTypeId() . ':' . $this->getTargetBundle() . ':' . $name;
371 $keys = array_flip(['type', 'label', 'settings', 'third_party_settings']);
372 $configuration['formatter'] = array_intersect_key($options, $keys);
375 $configuration['id'] = 'extra_field_block:' . $this->getTargetEntityTypeId() . ':' . $this->getTargetBundle() . ':' . $name;
378 $section = $this->getDefaultSection();
379 $region = isset($options['region']) ? $options['region'] : $section->getDefaultRegion();
380 $new_component = (new SectionComponent(\Drupal::service('uuid')->generate(), $region, $configuration));
381 $section->appendComponent($new_component);
387 * Gets a default section.
389 * @return \Drupal\layout_builder\Section
390 * The default section.
392 protected function getDefaultSection() {
393 // If no section exists, append a new one.
394 if (!$this->hasSection(0)) {
395 $this->appendSection(new Section('layout_onecol'));
398 // Return the first section.
399 return $this->getSection(0);