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\Section;
13 use Drupal\layout_builder\SectionComponent;
14 use Drupal\layout_builder\SectionStorage\SectionStorageTrait;
17 * Provides an entity view display entity that has a layout.
20 * Layout Builder is currently experimental and should only be leveraged by
21 * experimental modules and development releases of contributed modules.
22 * See https://www.drupal.org/core/experimental for more information.
24 class LayoutBuilderEntityViewDisplay extends BaseEntityViewDisplay implements LayoutEntityDisplayInterface {
26 use SectionStorageTrait;
29 * The entity field manager.
31 * @var \Drupal\Core\Entity\EntityFieldManagerInterface
33 protected $entityFieldManager;
38 public function __construct(array $values, $entity_type) {
39 // Set $entityFieldManager before calling the parent constructor because the
40 // constructor will call init() which then calls setComponent() which needs
41 // $entityFieldManager.
42 $this->entityFieldManager = \Drupal::service('entity_field.manager');
43 parent::__construct($values, $entity_type);
49 public function isOverridable() {
50 return $this->getThirdPartySetting('layout_builder', 'allow_custom', FALSE);
56 public function setOverridable($overridable = TRUE) {
57 $this->setThirdPartySetting('layout_builder', 'allow_custom', $overridable);
64 public function isLayoutBuilderEnabled() {
65 return (bool) $this->getThirdPartySetting('layout_builder', 'enabled');
71 public function enableLayoutBuilder() {
72 $this->setThirdPartySetting('layout_builder', 'enabled', TRUE);
79 public function disableLayoutBuilder() {
80 $this->setOverridable(FALSE);
81 $this->setThirdPartySetting('layout_builder', 'enabled', FALSE);
88 public function getSections() {
89 return $this->getThirdPartySetting('layout_builder', 'sections', []);
95 protected function setSections(array $sections) {
96 $this->setThirdPartySetting('layout_builder', 'sections', array_values($sections));
103 public function preSave(EntityStorageInterface $storage) {
104 parent::preSave($storage);
106 $original_value = isset($this->original) ? $this->original->isOverridable() : FALSE;
107 $new_value = $this->isOverridable();
108 if ($original_value !== $new_value) {
109 $entity_type_id = $this->getTargetEntityTypeId();
110 $bundle = $this->getTargetBundle();
113 $this->addSectionField($entity_type_id, $bundle, 'layout_builder__layout');
116 $this->removeSectionField($entity_type_id, $bundle, 'layout_builder__layout');
120 $already_enabled = isset($this->original) ? $this->original->isLayoutBuilderEnabled() : FALSE;
121 $set_enabled = $this->isLayoutBuilderEnabled();
122 if ($already_enabled !== $set_enabled) {
124 // Loop through all existing field-based components and add them as
125 // section-based components.
126 $components = $this->getComponents();
127 // Sort the components by weight.
128 uasort($components, 'Drupal\Component\Utility\SortArray::sortByWeightElement');
129 foreach ($components as $name => $component) {
130 $this->setComponent($name, $component);
134 // When being disabled, remove all existing section data.
135 while (count($this) > 0) {
136 $this->removeSection(0);
143 * Removes a layout section field if it is no longer needed.
145 * Because the field is shared across all view modes, the field will only be
146 * removed if no other view modes are using it.
148 * @param string $entity_type_id
149 * The entity type ID.
150 * @param string $bundle
152 * @param string $field_name
153 * The name for the layout section field.
155 protected function removeSectionField($entity_type_id, $bundle, $field_name) {
156 $query = $this->entityTypeManager()->getStorage($this->getEntityTypeId())->getQuery()
157 ->condition('targetEntityType', $this->getTargetEntityTypeId())
158 ->condition('bundle', $this->getTargetBundle())
159 ->condition('mode', $this->getMode(), '<>')
160 ->condition('third_party_settings.layout_builder.allow_custom', TRUE);
161 $enabled = (bool) $query->count()->execute();
162 if (!$enabled && $field = FieldConfig::loadByName($entity_type_id, $bundle, $field_name)) {
168 * Adds a layout section field to a given bundle.
170 * @param string $entity_type_id
171 * The entity type ID.
172 * @param string $bundle
174 * @param string $field_name
175 * The name for the layout section field.
177 protected function addSectionField($entity_type_id, $bundle, $field_name) {
178 $field = FieldConfig::loadByName($entity_type_id, $bundle, $field_name);
180 $field_storage = FieldStorageConfig::loadByName($entity_type_id, $field_name);
181 if (!$field_storage) {
182 $field_storage = FieldStorageConfig::create([
183 'entity_type' => $entity_type_id,
184 'field_name' => $field_name,
185 'type' => 'layout_section',
188 $field_storage->save();
191 $field = FieldConfig::create([
192 'field_storage' => $field_storage,
194 'label' => t('Layout'),
203 public function createCopy($mode) {
204 // Disable Layout Builder and remove any sections copied from the original.
205 return parent::createCopy($mode)
207 ->disableLayoutBuilder();
213 protected function getDefaultRegion() {
214 if ($this->hasSection(0)) {
215 return $this->getSection(0)->getDefaultRegion();
218 return parent::getDefaultRegion();
222 * Wraps the context repository service.
224 * @return \Drupal\Core\Plugin\Context\ContextRepositoryInterface
225 * The context repository service.
227 protected function contextRepository() {
228 return \Drupal::service('context.repository');
234 public function buildMultiple(array $entities) {
235 $build_list = parent::buildMultiple($entities);
236 if (!$this->isLayoutBuilderEnabled()) {
240 /** @var \Drupal\Core\Entity\EntityInterface $entity */
241 foreach ($entities as $id => $entity) {
242 $sections = $this->getRuntimeSections($entity);
244 foreach ($build_list[$id] as $name => $build_part) {
245 $field_definition = $this->getFieldDefinition($name);
246 if ($field_definition && $field_definition->isDisplayConfigurable($this->displayContext)) {
247 unset($build_list[$id][$name]);
251 // Bypass ::getContexts() in order to use the runtime entity, not a
253 $contexts = $this->contextRepository()->getAvailableContexts();
254 $label = new TranslatableMarkup('@entity being viewed', [
255 '@entity' => $entity->getEntityType()->getSingularLabel(),
257 $contexts['layout_builder.entity'] = EntityContext::fromEntity($entity, $label);
258 foreach ($sections as $delta => $section) {
259 $build_list[$id]['_layout_builder'][$delta] = $section->toRenderArray($contexts);
268 * Gets the runtime sections for a given entity.
270 * @param \Drupal\Core\Entity\FieldableEntityInterface $entity
273 * @return \Drupal\layout_builder\Section[]
276 protected function getRuntimeSections(FieldableEntityInterface $entity) {
277 if ($this->isOverridable() && !$entity->get('layout_builder__layout')->isEmpty()) {
278 return $entity->get('layout_builder__layout')->getSections();
281 return $this->getSections();
287 * @todo Move this upstream in https://www.drupal.org/node/2939931.
289 public function label() {
290 $bundle_info = \Drupal::service('entity_type.bundle.info')->getBundleInfo($this->getTargetEntityTypeId());
291 $bundle_label = $bundle_info[$this->getTargetBundle()]['label'];
292 $target_entity_type = $this->entityTypeManager()->getDefinition($this->getTargetEntityTypeId());
293 return new TranslatableMarkup('@bundle @label', ['@bundle' => $bundle_label, '@label' => $target_entity_type->getPluralLabel()]);
299 public function calculateDependencies() {
300 parent::calculateDependencies();
302 foreach ($this->getSections() as $delta => $section) {
303 $this->calculatePluginDependencies($section->getLayout());
304 foreach ($section->getComponents() as $uuid => $component) {
305 $this->calculatePluginDependencies($component->getPlugin());
315 public function onDependencyRemoval(array $dependencies) {
316 $changed = parent::onDependencyRemoval($dependencies);
318 // Loop through all sections and determine if the removed dependencies are
319 // used by their layout plugins.
320 foreach ($this->getSections() as $delta => $section) {
321 $layout_dependencies = $this->getPluginDependencies($section->getLayout());
322 $layout_removed_dependencies = $this->getPluginRemovedDependencies($layout_dependencies, $dependencies);
323 if ($layout_removed_dependencies) {
324 // @todo Allow the plugins to react to their dependency removal in
325 // https://www.drupal.org/project/drupal/issues/2579743.
326 $this->removeSection($delta);
329 // If the section is not removed, loop through all components.
331 foreach ($section->getComponents() as $uuid => $component) {
332 $plugin_dependencies = $this->getPluginDependencies($component->getPlugin());
333 $component_removed_dependencies = $this->getPluginRemovedDependencies($plugin_dependencies, $dependencies);
334 if ($component_removed_dependencies) {
335 // @todo Allow the plugins to react to their dependency removal in
336 // https://www.drupal.org/project/drupal/issues/2579743.
337 $section->removeComponent($uuid);
349 public function setComponent($name, array $options = []) {
350 parent::setComponent($name, $options);
352 // Only continue if Layout Builder is enabled.
353 if (!$this->isLayoutBuilderEnabled()) {
357 // Retrieve the updated options after the parent:: call.
358 $options = $this->content[$name];
359 // Provide backwards compatibility by converting to a section component.
360 $field_definition = $this->getFieldDefinition($name);
361 $extra_fields = $this->entityFieldManager->getExtraFields($this->getTargetEntityTypeId(), $this->getTargetBundle());
362 $is_view_configurable_non_extra_field = $field_definition && $field_definition->isDisplayConfigurable('view') && isset($options['type']);
363 if ($is_view_configurable_non_extra_field || isset($extra_fields['display'][$name])) {
365 'label_display' => '0',
366 'context_mapping' => ['entity' => 'layout_builder.entity'],
368 if ($is_view_configurable_non_extra_field) {
369 $configuration['id'] = 'field_block:' . $this->getTargetEntityTypeId() . ':' . $this->getTargetBundle() . ':' . $name;
370 $keys = array_flip(['type', 'label', 'settings', 'third_party_settings']);
371 $configuration['formatter'] = array_intersect_key($options, $keys);
374 $configuration['id'] = 'extra_field_block:' . $this->getTargetEntityTypeId() . ':' . $this->getTargetBundle() . ':' . $name;
377 $section = $this->getDefaultSection();
378 $region = isset($options['region']) ? $options['region'] : $section->getDefaultRegion();
379 $new_component = (new SectionComponent(\Drupal::service('uuid')->generate(), $region, $configuration));
380 $section->appendComponent($new_component);
386 * Gets a default section.
388 * @return \Drupal\layout_builder\Section
389 * The default section.
391 protected function getDefaultSection() {
392 // If no section exists, append a new one.
393 if (!$this->hasSection(0)) {
394 $this->appendSection(new Section('layout_onecol'));
397 // Return the first section.
398 return $this->getSection(0);