diff --git a/core/composer.json b/core/composer.json index c3600ab..e399eb2 100644 --- a/core/composer.json +++ b/core/composer.json @@ -110,6 +110,7 @@ "drupal/image": "self.version", "drupal/inline_form_errors": "self.version", "drupal/language": "self.version", + "drupal/layout_builder": "self.version", "drupal/layout_discovery": "self.version", "drupal/link": "self.version", "drupal/locale": "self.version", diff --git a/core/modules/layout_builder/layout_builder.info.yml b/core/modules/layout_builder/layout_builder.info.yml new file mode 100644 index 0000000..50a51d0 --- /dev/null +++ b/core/modules/layout_builder/layout_builder.info.yml @@ -0,0 +1,8 @@ +name: 'Layout Builder' +type: module +description: 'Provides layout building utility.' +package: Core (Experimental) +version: VERSION +core: 8.x +dependencies: + - layout_discovery diff --git a/core/modules/layout_builder/layout_builder.module b/core/modules/layout_builder/layout_builder.module new file mode 100644 index 0000000..ab80ff5 --- /dev/null +++ b/core/modules/layout_builder/layout_builder.module @@ -0,0 +1,19 @@ +' . t('About') . ''; + $output .= '

' . t('Layout Builder provides layout building utility, surprisingly.') . '

'; + $output .= '

' . t('For more information, see the online documentation for the Layout Builder module.', [':layout-builder-documentation' => 'https://www.drupal.org/docs/8/core/modules/layout_builder']) . '

'; + return $output; + } +} diff --git a/core/modules/layout_builder/src/LayoutSectionItemInterface.php b/core/modules/layout_builder/src/LayoutSectionItemInterface.php new file mode 100644 index 0000000..1a39cde --- /dev/null +++ b/core/modules/layout_builder/src/LayoutSectionItemInterface.php @@ -0,0 +1,15 @@ +account = $account; + $this->layoutPluginManager = $layoutPluginManager; + $this->blockManager = $blockManager; + $this->contextHandler = $context_handler; + $this->contextRepository = $context_repository; + parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $container->get('current_user'), + $container->get('plugin.manager.core.layout'), + $container->get('plugin.manager.block'), + $container->get('context.handler'), + $container->get('context.repository'), + $plugin_id, + $plugin_definition, + $configuration['field_definition'], + $configuration['settings'], + $configuration['label'], + $configuration['view_mode'], + $configuration['third_party_settings'] + ); + } + + /** + * {@inheritdoc} + */ + public function viewElements(FieldItemListInterface $items, $langcode) { + $elements = []; + + foreach ($items as $delta => $item) { + $elements[$delta] = $this->buildSection($item); + } + + return $elements; + } + + /** + * Builds the render array for the layout section. + * + * @param \Drupal\layout_builder\LayoutSectionItemInterface $item + * The layout section item. + * + * @return array + * A render array for the field item. + */ + protected function buildSection(LayoutSectionItemInterface $item) { + /** @var \Drupal\Core\Layout\LayoutInterface $layout */ + $layout = $this->layoutPluginManager->createInstance($item->layout); + $regions = []; + foreach ($item->section as $region => $blocks) { + foreach ($blocks as $uuid => $configuration) { + /** @var \Drupal\Core\Block\BlockPluginInterface $block */ + $block = $this->blockManager->createInstance($configuration['plugin_id'], $configuration); + if ($block instanceof ContextAwarePluginInterface) { + $contexts = $this->contextRepository->getRuntimeContexts(array_values($block->getContextMapping())); + $this->contextHandler->applyContextMapping($block, $contexts); + } + $access = $block->access($this->account, TRUE); + if ($access->isAllowed()) { + $regions[$region][$uuid] = $block->build(); + } + } + } + return $layout->build($regions); + } + +} diff --git a/core/modules/layout_builder/src/Plugin/Field/FieldType/LayoutSectionItem.php b/core/modules/layout_builder/src/Plugin/Field/FieldType/LayoutSectionItem.php new file mode 100644 index 0000000..8cf547d --- /dev/null +++ b/core/modules/layout_builder/src/Plugin/Field/FieldType/LayoutSectionItem.php @@ -0,0 +1,87 @@ +setLabel(new TranslatableMarkup('Layout')) + ->setSetting('case_sensitive', FALSE) + ->setRequired(TRUE); + $properties[static::mainPropertyName()] = MapDataDefinition::create('map') + ->setLabel(new TranslatableMarkup('Layout Section')) + ->setRequired(FALSE); + + return $properties; + } + + /** + * {@inheritdoc} + */ + public static function mainPropertyName() { + return 'section'; + } + + /** + * {@inheritdoc} + */ + public static function schema(FieldStorageDefinitionInterface $field_definition) { + $schema = [ + 'columns' => [ + 'layout' => [ + 'type' => 'varchar', + 'length' => '255', + 'binary' => FALSE, + ], + static::mainPropertyName() => [ + 'type' => 'blob', + 'size' => 'normal', + 'serialize' => TRUE, + ], + ], + ]; + + return $schema; + } + + /** + * {@inheritdoc} + */ + public static function generateSampleValue(FieldDefinitionInterface $field_definition) { + $values['layout'] = 'layout_onecol'; + $values[static::mainPropertyName()] = []; + return $values; + } + + /** + * {@inheritdoc} + */ + public function isEmpty() { + $sections = $this->get(static::mainPropertyName())->getValue(); + return empty($sections); + } + +} diff --git a/core/modules/layout_builder/tests/src/Kernel/LayoutSectionFormatterTest.php b/core/modules/layout_builder/tests/src/Kernel/LayoutSectionFormatterTest.php new file mode 100644 index 0000000..da5c9b5 --- /dev/null +++ b/core/modules/layout_builder/tests/src/Kernel/LayoutSectionFormatterTest.php @@ -0,0 +1,212 @@ +installConfig(['field']); + $this->installSchema('system', ['sequences']); + $this->installEntitySchema('entity_test'); + $this->installEntitySchema('user'); + + $entity_type = 'entity_test'; + $bundle = $entity_type; + $this->fieldName = 'field_my_sections'; + + $field_storage = FieldStorageConfig::create([ + 'field_name' => $this->fieldName, + 'entity_type' => $entity_type, + 'type' => 'layout_section', + ]); + $field_storage->save(); + + $instance = FieldConfig::create([ + 'field_storage' => $field_storage, + 'bundle' => $bundle, + 'label' => 'My Sections', + ]); + $instance->save(); + + $this->display = EntityViewDisplay::create([ + 'targetEntityType' => $entity_type, + 'bundle' => $bundle, + 'mode' => 'default', + 'status' => TRUE, + ]); + $this->display->setComponent($this->fieldName, [ + 'type' => 'layout_section', + 'settings' => [], + ]); + $this->display->save(); + + $test_user = User::create([ + 'name' => 'foobar', + 'mail' => 'foobar@example.com', + ]); + $test_user->save(); + $this->container->get('current_user')->setAccount($test_user); + } + + /** + * Tests layout_section formatter output. + * + * @dataProvider providerTestLayoutSectionFormatter + */ + public function testLayoutSectionFormatter($layout_data, $expected_selector, $expected_content) { + $values = []; + $values[$this->fieldName] = $layout_data; + $entity = EntityTest::create($values); + + // Build and render the content. + $content = $this->display->build($entity); + $this->render($content); + // Pass the main content to the assertions to help with debugging. + $main_content = $this->cssSelect('main')[0]->asXML(); + + // Find the given selector. + foreach ((array) $expected_selector as $selector) { + $element = $this->cssSelect($selector); + $this->assertNotEmpty($element, $main_content); + } + + // Find the given content. + foreach ((array) $expected_content as $content) { + $this->assertRaw($content, $main_content); + } + } + + /** + * Provides test data to ::testLayoutSectionFormatter(). + */ + public function providerTestLayoutSectionFormatter() { + $data = []; + $data['block_with_context'] = [ + [ + [ + 'layout' => 'layout_onecol', + 'section' => [ + 'content' => [ + 'baz' => [ + 'plugin_id' => 'test_context_aware', + 'context_mapping' => [ + 'user' => '@user.current_user_context:current_user', + ], + ], + ], + ], + ], + ], + [ + '.layout--onecol', + '#test_context_aware--username', + ], + [ + 'foobar', + 'User context found', + ], + ]; + $data['single_section_single_block'] = [ + [ + [ + 'layout' => 'layout_onecol', + 'section' => [ + 'content' => [ + 'baz' => [ + 'plugin_id' => 'system_powered_by_block', + ], + ], + ], + ], + ], + '.layout--onecol', + 'Powered by', + ]; + $data['multiple_sections'] = [ + [ + [ + 'layout' => 'layout_onecol', + 'section' => [ + 'content' => [ + 'baz' => [ + 'plugin_id' => 'system_powered_by_block', + ], + ], + ], + ], + [ + 'layout' => 'layout_twocol', + 'section' => [ + 'left' => [ + 'foo' => [ + 'plugin_id' => 'test_block_instantiation', + 'display_message' => 'foo text', + ], + ], + 'right' => [ + 'bar' => [ + 'plugin_id' => 'test_block_instantiation', + 'display_message' => 'bar text', + ], + ], + ], + ], + ], + [ + '.layout--onecol', + '.layout--twocol', + ], + [ + 'Powered by', + 'foo text', + 'bar text', + ], + ]; + return $data; + } + +}