diff --git a/core/modules/layout_builder/layout_builder.services.yml b/core/modules/layout_builder/layout_builder.services.yml index b2ee1fe13e..98740eb9b5 100644 --- a/core/modules/layout_builder/layout_builder.services.yml +++ b/core/modules/layout_builder/layout_builder.services.yml @@ -58,3 +58,8 @@ services: class: Drupal\layout_builder\Controller\LayoutBuilderHtmlEntityFormController public: false arguments: ['@layout_builder.controller.entity_form.inner'] + layout_builder.element.prepare_layout: + class: Drupal\layout_builder\EventSubscriber\PrepareLayout + arguments: ['@layout_builder.tempstore_repository', '@messenger'] + tags: + - { name: event_subscriber } diff --git a/core/modules/layout_builder/src/Element/LayoutBuilder.php b/core/modules/layout_builder/src/Element/LayoutBuilder.php index 80dcff90f7..f24c56314b 100644 --- a/core/modules/layout_builder/src/Element/LayoutBuilder.php +++ b/core/modules/layout_builder/src/Element/LayoutBuilder.php @@ -3,18 +3,18 @@ namespace Drupal\layout_builder\Element; use Drupal\Core\Ajax\AjaxHelperTrait; -use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Plugin\PluginFormInterface; use Drupal\Core\Render\Element; use Drupal\Core\Render\Element\RenderElement; use Drupal\Core\Url; use Drupal\layout_builder\Context\LayoutBuilderContextTrait; +use Drupal\layout_builder\Event\PrepareLayoutEvent; +use Drupal\layout_builder\LayoutBuilderEvents; use Drupal\layout_builder\LayoutBuilderHighlightTrait; -use Drupal\layout_builder\LayoutTempstoreRepositoryInterface; -use Drupal\layout_builder\OverridesSectionStorageInterface; use Drupal\layout_builder\SectionStorageInterface; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** * Defines a render element for building the Layout Builder UI. @@ -31,18 +31,11 @@ class LayoutBuilder extends RenderElement implements ContainerFactoryPluginInter use LayoutBuilderHighlightTrait; /** - * The layout tempstore repository. + * The Event Dispatcher. * - * @var \Drupal\layout_builder\LayoutTempstoreRepositoryInterface + * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface */ - protected $layoutTempstoreRepository; - - /** - * The messenger service. - * - * @var \Drupal\Core\Messenger\MessengerInterface - */ - protected $messenger; + protected $eventDispatcher; /** * Constructs a new LayoutBuilder. @@ -53,15 +46,24 @@ class LayoutBuilder extends RenderElement implements ContainerFactoryPluginInter * The plugin ID for the plugin instance. * @param mixed $plugin_definition * The plugin implementation definition. - * @param \Drupal\layout_builder\LayoutTempstoreRepositoryInterface $layout_tempstore_repository - * The layout tempstore repository. - * @param \Drupal\Core\Messenger\MessengerInterface $messenger - * The messenger service. + * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher + * The event dispatcher service. + * @param \Drupal\Core\Messenger\MessengerInterface|NULL $messenger + * The messenger service. This is left optional for BC reasons and + * will be deprecated in drupal:9.1.0. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, LayoutTempstoreRepositoryInterface $layout_tempstore_repository, MessengerInterface $messenger) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, $event_dispatcher, $messenger = NULL) { parent::__construct($configuration, $plugin_id, $plugin_definition); - $this->layoutTempstoreRepository = $layout_tempstore_repository; - $this->messenger = $messenger; + + if (!($event_dispatcher instanceof EventDispatcherInterface)) { + @trigger_error('The event_dispatcher service should be passed to LayoutBuilder::__construct() instead of the layout_builder.tempstore_repository service since 9.1.0. This will be required in Drupal 10.0.0.', E_USER_DEPRECATED); + $event_dispatcher = \Drupal::service('event_dispatcher'); + } + $this->eventDispatcher = $event_dispatcher; + + if ($messenger) { + @trigger_error('Calling LayoutBuilder::__construct() with the $messenger argument is deprecated in drupal:9.1.0 and will be removed in drupal:10.0.0', E_USER_DEPRECATED); + } } /** @@ -72,8 +74,8 @@ public static function create(ContainerInterface $container, array $configuratio $configuration, $plugin_id, $plugin_definition, - $container->get('layout_builder.tempstore_repository'), - $container->get('messenger') + $container->get('event_dispatcher'), + NULL ); } @@ -145,19 +147,8 @@ protected function layout(SectionStorageInterface $section_storage) { * The section storage. */ protected function prepareLayout(SectionStorageInterface $section_storage) { - // If the layout has pending changes, add a warning. - if ($this->layoutTempstoreRepository->has($section_storage)) { - $this->messenger->addWarning($this->t('You have unsaved changes.')); - } - // If the layout is an override that has not yet been overridden, copy the - // sections from the corresponding default. - elseif ($section_storage instanceof OverridesSectionStorageInterface && !$section_storage->isOverridden()) { - $sections = $section_storage->getDefaultSectionStorage()->getSections(); - foreach ($sections as $section) { - $section_storage->appendSection($section); - } - $this->layoutTempstoreRepository->set($section_storage); - } + $event = new PrepareLayoutEvent($section_storage); + $this->eventDispatcher->dispatch(LayoutBuilderEvents::PREPARE_LAYOUT, $event); } /** diff --git a/core/modules/layout_builder/src/Event/PrepareLayoutEvent.php b/core/modules/layout_builder/src/Event/PrepareLayoutEvent.php new file mode 100644 index 0000000000..bd42e8b2df --- /dev/null +++ b/core/modules/layout_builder/src/Event/PrepareLayoutEvent.php @@ -0,0 +1,45 @@ +sectionStorage = $section_storage; + } + + /** + * Gets the section storage. + * + * @return \Drupal\layout_builder\SectionStorageInterface + * The section storage. + */ + public function getSectionStorage() { + return $this->sectionStorage; + } + +} diff --git a/core/modules/layout_builder/src/EventSubscriber/PrepareLayout.php b/core/modules/layout_builder/src/EventSubscriber/PrepareLayout.php new file mode 100644 index 0000000000..2c6b6a17ba --- /dev/null +++ b/core/modules/layout_builder/src/EventSubscriber/PrepareLayout.php @@ -0,0 +1,79 @@ +layoutTempstoreRepository = $layout_tempstore_repository; + $this->messenger = $messenger; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + $events[LayoutBuilderEvents::PREPARE_LAYOUT][] = ['onPrepareLayout', 10]; + return $events; + } + + /** + * Prepares a layout for use in the UI. + * + * @param \Drupal\layout_builder\Event\PrepareLayoutEvent $event + * The prepare layout event. + */ + public function onPrepareLayout(PrepareLayoutEvent $event) { + $section_storage = $event->getSectionStorage(); + + // If the layout has pending changes, add a warning. + if ($this->layoutTempstoreRepository->has($section_storage)) { + $this->messenger->addWarning($this->t('You have unsaved changes.')); + } + // If the layout is an override that has not yet been overridden, copy the + // sections from the corresponding default. + elseif ($section_storage instanceof OverridesSectionStorageInterface && !$section_storage->isOverridden()) { + $sections = $section_storage->getDefaultSectionStorage()->getSections(); + foreach ($sections as $section) { + $section_storage->appendSection($section); + } + $this->layoutTempstoreRepository->set($section_storage); + } + } + +} diff --git a/core/modules/layout_builder/src/LayoutBuilderEvents.php b/core/modules/layout_builder/src/LayoutBuilderEvents.php index 5b19e1c198..19b5c2a541 100644 --- a/core/modules/layout_builder/src/LayoutBuilderEvents.php +++ b/core/modules/layout_builder/src/LayoutBuilderEvents.php @@ -26,4 +26,19 @@ final class LayoutBuilderEvents { */ const SECTION_COMPONENT_BUILD_RENDER_ARRAY = 'section_component.build.render_array'; + /** + * Name of the event fired when preparing a LayoutBuilder Element. + * + * This event allows modules to collabourate on creating the sections used in + * a LayoutBuilder Element during PreRender(). + * + * @EVENT + * + * @see \Drupal\layout_builder\Event\PrepareLayoutEvent + * @see \Drupal\layout_builder\Element\LayoutBuilder + * + * @var string + */ + const PREPARE_LAYOUT = 'prepare_layout'; + } diff --git a/core/modules/layout_builder/tests/modules/layout_builder_element_test/layout_builder_element_test.info.yml b/core/modules/layout_builder/tests/modules/layout_builder_element_test/layout_builder_element_test.info.yml new file mode 100644 index 0000000000..8d92f61a10 --- /dev/null +++ b/core/modules/layout_builder/tests/modules/layout_builder_element_test/layout_builder_element_test.info.yml @@ -0,0 +1,8 @@ +name: 'Layout Builder element test' +type: module +description: 'Support module for testing the layout builder element.' +package: Testing +version: VERSION +core: 8.x +dependencies: + - drupal:layout_builder diff --git a/core/modules/layout_builder/tests/modules/layout_builder_element_test/layout_builder_element_test.services.yml b/core/modules/layout_builder/tests/modules/layout_builder_element_test/layout_builder_element_test.services.yml new file mode 100644 index 0000000000..ce75a2e892 --- /dev/null +++ b/core/modules/layout_builder/tests/modules/layout_builder_element_test/layout_builder_element_test.services.yml @@ -0,0 +1,6 @@ +services: + layout_builder_element_test.prepare_layout: + class: Drupal\layout_builder_element_test\EventSubscriber\TestPrepareLayout + arguments: ['@layout_builder.tempstore_repository', '@messenger'] + tags: + - { name: event_subscriber } diff --git a/core/modules/layout_builder/tests/modules/layout_builder_element_test/src/EventSubscriber/TestPrepareLayout.php b/core/modules/layout_builder/tests/modules/layout_builder_element_test/src/EventSubscriber/TestPrepareLayout.php new file mode 100644 index 0000000000..3b50dc0716 --- /dev/null +++ b/core/modules/layout_builder/tests/modules/layout_builder_element_test/src/EventSubscriber/TestPrepareLayout.php @@ -0,0 +1,118 @@ +layoutTempstoreRepository = $layout_tempstore_repository; + $this->messenger = $messenger; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + // Act before LB subscriber. + $events[LayoutBuilderEvents::PREPARE_LAYOUT][] = ['onBeforePrepareLayout', 20]; + // Act after LB subscriber. + $events[LayoutBuilderEvents::PREPARE_LAYOUT][] = ['onAfterPrepareLayout', -10]; + return $events; + } + + /** + * Subscriber to test acting before the LB subscriber. + * + * @param \Drupal\layout_builder\Event\PrepareLayoutEvent $event + * The prepare layout event. + */ + public function onBeforePrepareLayout(PrepareLayoutEvent $event) { + $section_storage = $event->getSectionStorage(); + $context = $section_storage->getContextValues(); + + if (!empty($context['entity'])) { + /** @var \Drupal\Core\Entity\EntityInterface $entity */ + $entity = $context['entity']; + + // Node 1 or 2: Append a block to the layout. + if (in_array($entity->id(), ['1', '2'])) { + $section = new Section('layout_onecol'); + $section->appendComponent(new SectionComponent('fake-uuid', 'content', [ + 'id' => 'static_block', + 'label' => 'Test static block title', + 'label_display' => 'visible', + ])); + $section_storage->appendSection($section); + } + + // Node 2: Stop event propagation. + if ($entity->id() === '2') { + $event->stopPropagation(); + } + } + } + + /** + * Subscriber to test acting after the LB subscriber. + * + * @param \Drupal\layout_builder\Event\PrepareLayoutEvent $event + * The prepare layout event. + */ + public function onAfterPrepareLayout(PrepareLayoutEvent $event) { + $section_storage = $event->getSectionStorage(); + $context = $section_storage->getContextValues(); + + if (!empty($context['entity'])) { + /** @var \Drupal\Core\Entity\EntityInterface $entity */ + $entity = $context['entity']; + + // Node 1, 2, or 3: Append a block to the layout. + if (in_array($entity->id(), ['1', '2', '3'])) { + $section = new Section('layout_onecol'); + $section->appendComponent(new SectionComponent('fake-uuid', 'content', [ + 'id' => 'static_block_two', + 'label' => 'Test second static block title', + 'label_display' => 'visible', + ])); + $section_storage->appendSection($section); + } + } + } + +} diff --git a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderPrepareLayoutTest.php b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderPrepareLayoutTest.php new file mode 100644 index 0000000000..95c5b2e24c --- /dev/null +++ b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderPrepareLayoutTest.php @@ -0,0 +1,119 @@ +createContentType(['type' => 'bundle_with_section_field']); + LayoutBuilderEntityViewDisplay::load('node.bundle_with_section_field.default') + ->enableLayoutBuilder() + ->setOverridable() + ->save(); + + $this->createNode([ + 'type' => 'bundle_with_section_field', + 'title' => 'The first node title', + 'body' => [ + [ + 'value' => 'The first node body', + ], + ], + ]); + $this->createNode([ + 'type' => 'bundle_with_section_field', + 'title' => 'The second node title', + 'body' => [ + [ + 'value' => 'The second node body', + ], + ], + ]); + $this->createNode([ + 'type' => 'bundle_with_section_field', + 'title' => 'The third node title', + 'body' => [ + [ + 'value' => 'The third node body', + ], + ], + ]); + } + + /** + * Tests that we can alter a Layout Builder element while preparing. + * + * @see \Drupal\layout_builder_element_test\EventSubscriber\TestPrepareLayout; + */ + public function testAlterPrepareLayout() { + $assert_session = $this->assertSession(); + $page = $this->getSession()->getPage(); + + $this->drupalLogin($this->drupalCreateUser([ + 'access content', + 'configure any layout', + 'administer node display', + 'configure all bundle_with_section_field node layout overrides', + ])); + + // Add a block to the defaults. + $this->drupalGet('admin/structure/types/manage/bundle_with_section_field/display/default'); + $page->clickLink('Manage layout'); + $page->clickLink('Add block'); + $page->clickLink('Powered by Drupal'); + $page->fillField('settings[label]', 'Default block title'); + $page->checkField('settings[label_display]'); + $page->pressButton('Add block'); + $page->pressButton('Save layout'); + + // Chack the block is on the node page. + $this->drupalGet('node/1'); + $assert_session->pageTextContains('Default block title'); + + // When we edit the layout, it get's the static blocks. + $this->drupalGet('node/1/layout'); + $assert_session->pageTextContains('Test static block title'); + $assert_session->pageTextNotContains('Default block title'); + $assert_session->pageTextContains('Test second static block title'); + + // When we edit the second node, only the first event fires. + $this->drupalGet('node/2/layout'); + $assert_session->pageTextContains('Test static block title'); + $assert_session->pageTextNotContains('Default block title'); + $assert_session->pageTextNotContains('Test second static block title'); + + // When we edit the third node, the default exists PLUS our static block. + $this->drupalGet('node/3/layout'); + $assert_session->pageTextNotContains('Test static block title'); + $assert_session->pageTextContains('Default block title'); + $assert_session->pageTextContains('Test second static block title'); + } + +}