.
+ *
+ * @ingroup themeable
+ */
+#}
+{%
+set classes = [
+'field-layout--onecol',
+]
+%}
+{% if content %}
+
+{% endif %}
diff --git a/core/modules/field_layout/layouts/twocol/field-layout--twocol.html.twig b/core/modules/field_layout/layouts/twocol/field-layout--twocol.html.twig
new file mode 100644
index 0000000..4dffc01
--- /dev/null
+++ b/core/modules/field_layout/layouts/twocol/field-layout--twocol.html.twig
@@ -0,0 +1,28 @@
+{#
+/**
+ * @file
+ * Default theme implementation to display a two column layout.
+ *
+ * Available variables:
+ * - content: The content for this layout.
+ * - attributes: HTML attributes for the layout
.
+ *
+ * @ingroup themeable
+ */
+#}
+{%
+set classes = [
+'field-layout--twocol',
+]
+%}
+{% if content %}
+
+
+ {{ content.left }}
+
+
+
+ {{ content.right }}
+
+
+{% endif %}
diff --git a/core/modules/field_layout/layouts/twocol/twocol.layout.css b/core/modules/field_layout/layouts/twocol/twocol.layout.css
new file mode 100644
index 0000000..8e2f623
--- /dev/null
+++ b/core/modules/field_layout/layouts/twocol/twocol.layout.css
@@ -0,0 +1,14 @@
+.field-layout--twocol {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+}
+.field-layout--twocol > .field-layout-region {
+ flex: 0 1 50%;
+ max-width: 50%;
+}
+
+.field-layout--twocol > .field-layout-region--left {
+ max-width: calc(50% - 10px);
+ margin-right: 10px;
+}
diff --git a/core/modules/field_layout/src/Display/EntityDisplayWithLayoutInterface.php b/core/modules/field_layout/src/Display/EntityDisplayWithLayoutInterface.php
new file mode 100644
index 0000000..15204ac
--- /dev/null
+++ b/core/modules/field_layout/src/Display/EntityDisplayWithLayoutInterface.php
@@ -0,0 +1,56 @@
+getDefinition($layout_id);
+ }
+
+ /**
+ * Implements \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface::getLayoutId().
+ */
+ public function getLayoutId() {
+ return $this->getThirdPartySetting('field_layout', 'id');
+ }
+
+ /**
+ * Implements \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface::getLayoutSettings().
+ */
+ public function getLayoutSettings() {
+ return $this->getThirdPartySetting('field_layout', 'settings', []);
+ }
+
+ /**
+ * Implements \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface::setLayout().
+ */
+ public function setLayout($layout_id, array $layout_settings = []) {
+ $this->setThirdPartySetting('field_layout', 'id', $layout_id);
+ $this->setThirdPartySetting('field_layout', 'settings', $layout_settings);
+ return $this;
+ }
+
+ /**
+ * Implements \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface::getLayoutPlugin().
+ */
+ public function getLayoutPlugin() {
+ return \Drupal::service('plugin.manager.layout_plugin')->createInstance($this->getLayoutId(), $this->getLayoutSettings());
+ }
+
+ /**
+ * Overrides \Drupal\Core\Entity\EntityDisplayBase::preSave().
+ */
+ public function preSave(EntityStorageInterface $storage) {
+ if (!$this->getLayoutId()) {
+ $this->setLayout('onecol');
+ }
+
+ parent::preSave($storage);
+ }
+
+}
diff --git a/core/modules/field_layout/src/Entity/FieldLayoutEntityFormDisplay.php b/core/modules/field_layout/src/Entity/FieldLayoutEntityFormDisplay.php
new file mode 100644
index 0000000..2f3111e
--- /dev/null
+++ b/core/modules/field_layout/src/Entity/FieldLayoutEntityFormDisplay.php
@@ -0,0 +1,25 @@
+getLayoutDefinition($this->getLayoutId() ?: 'onecol')->getDefaultRegion();
+ }
+
+}
diff --git a/core/modules/field_layout/src/Entity/FieldLayoutEntityViewDisplay.php b/core/modules/field_layout/src/Entity/FieldLayoutEntityViewDisplay.php
new file mode 100644
index 0000000..b527059
--- /dev/null
+++ b/core/modules/field_layout/src/Entity/FieldLayoutEntityViewDisplay.php
@@ -0,0 +1,25 @@
+getLayoutDefinition($this->getLayoutId() ?: 'onecol')->getDefaultRegion();
+ }
+
+}
diff --git a/core/modules/field_layout/src/FieldLayoutBuilder.php b/core/modules/field_layout/src/FieldLayoutBuilder.php
new file mode 100644
index 0000000..be5e949
--- /dev/null
+++ b/core/modules/field_layout/src/FieldLayoutBuilder.php
@@ -0,0 +1,143 @@
+layoutPluginManager = $layout_plugin_manager;
+ $this->entityFieldManager = $entity_field_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('plugin.manager.layout_plugin'),
+ $container->get('entity_field.manager')
+ );
+ }
+
+ /**
+ * Applies the layout to an entity build.
+ *
+ * @param array $build
+ * A renderable array representing the entity content or form.
+ * @param \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface $display
+ * The entity display holding the display options configured for the entity
+ * components.
+ * @param string $display_context
+ * The display context, either 'form' or 'view'. If in a 'form' context, an
+ * alternate method will be used to render fields in their regions.
+ */
+ public function build(array &$build, EntityDisplayWithLayoutInterface $display, $display_context) {
+ $layout_definition = $this->layoutPluginManager->getDefinition($display->getLayoutId(), FALSE);
+ if ($layout_definition && $fields = $this->getFields($build, $display, $display_context)) {
+ // Add the regions to the $build in the correct order.
+ $fill = [];
+ if ($display_context === 'form') {
+ $fill['#process'][] = '\Drupal\Core\Render\Element\RenderElement::processGroup';
+ $fill['#pre_render'][] = '\Drupal\Core\Render\Element\RenderElement::preRenderGroup';
+ }
+ $regions = array_fill_keys($layout_definition->getRegionNames(), $fill);
+
+ foreach ($fields as $name => $field) {
+ // If this is a form, #group can be used to relocate the fields. This
+ // avoids breaking hook_form_alter() implementations by not actually
+ // moving the field in the form structure.
+ if ($display_context === 'form') {
+ $build[$name]['#group'] = $field['region'];
+ }
+ // Otherwise, move the field from the top-level of $build into a
+ // region-specific section.
+ else {
+ $regions[$field['region']][$name] = $build[$name];
+ unset($build[$name]);
+ }
+ }
+ $build['field_layout'] = $display->getLayoutPlugin()->build($regions);
+ $build['field_layout']['#process'][] = [static::class, 'processContent'];
+ }
+ }
+
+ /**
+ * Moves the content so that it can be processed properly by the form builder.
+ */
+ public static function processContent($element) {
+ $element['#pre_render'][] = [static::class, 'preRenderContent'];
+ $element['content'] = $element['#content'];
+ unset($element['#content']);
+ return $element;
+ }
+
+ /**
+ * Moves the content back to the location expected by the #theme hook.
+ */
+ public static function preRenderContent($element) {
+ $element['#content'] = $element['content'];
+ unset($element['content']);
+ return $element;
+ }
+
+ /**
+ * Gets the fields that need to be processed.
+ *
+ * @param array $build
+ * A renderable array representing the entity content or form.
+ * @param \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface $display
+ * The entity display holding the display options configured for the entity
+ * components.
+ * @param string $display_context
+ * The display context, either 'form' or 'view'.
+ *
+ * @return array
+ * An array of configurable fields present in the build.
+ */
+ protected function getFields(array $build, EntityDisplayWithLayoutInterface $display, $display_context) {
+ $components = $display->getComponents();
+
+ $field_definitions = $this->entityFieldManager->getFieldDefinitions($display->getTargetEntityTypeId(), $display->getTargetBundle());
+ $non_configurable_fields = array_filter($field_definitions, function (FieldDefinitionInterface $field_definition) use ($display_context) {
+ return !$field_definition->isDisplayConfigurable($display_context);
+ });
+ // Remove non-configurable fields.
+ $components = array_diff_key($components, $non_configurable_fields);
+
+ // Only include fields present in the build.
+ $components = array_intersect_key($components, $build);
+
+ return $components;
+ }
+
+}
diff --git a/core/modules/field_layout/src/Form/FieldLayoutEntityDisplayFormTrait.php b/core/modules/field_layout/src/Form/FieldLayoutEntityDisplayFormTrait.php
new file mode 100644
index 0000000..b7fd07e
--- /dev/null
+++ b/core/modules/field_layout/src/Form/FieldLayoutEntityDisplayFormTrait.php
@@ -0,0 +1,191 @@
+layoutPluginManager->getDefinition($this->getEntity()->getLayoutId() ?: 'onecol');
+ foreach ($layout_definition->getRegions() as $name => $region) {
+ $regions[$name] = [
+ 'title' => $region['label'],
+ 'message' => $this->t('No field is displayed.')
+ ];
+ }
+
+ $regions['hidden'] = [
+ 'title' => $this->t('Disabled', [], ['context' => 'Plural']),
+ 'message' => $this->t('No field is hidden.')
+ ];
+
+ return $regions;
+ }
+
+ /**
+ * Overrides \Drupal\field_ui\Form\EntityDisplayFormBase::form().
+ */
+ public function form(array $form, FormStateInterface $form_state) {
+ $form = parent::form($form, $form_state);
+
+ $form['field_layouts'] = [
+ '#type' => 'details',
+ '#title' => $this->t('Layout settings'),
+ ];
+
+ $layout_plugin = $this->getLayoutPlugin($this->getEntity(), $form_state);
+
+ $form['field_layouts']['field_layout'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Select a layout'),
+ '#options' => $this->layoutPluginManager->getLayoutOptions(),
+ '#default_value' => $layout_plugin->getPluginId(),
+ '#ajax' => [
+ 'callback' => '::settingsAjax',
+ 'wrapper' => 'field-layout-settings-wrapper',
+ 'trigger_as' => ['name' => 'field_layout_change'],
+ ],
+ ];
+ $form['field_layouts']['submit'] = [
+ '#type' => 'submit',
+ '#name' => 'field_layout_change',
+ '#value' => $this->t('Change layout'),
+ '#submit' => ['::settingsAjaxSubmit'],
+ '#attributes' => ['class' => ['js-hide']],
+ '#ajax' => [
+ 'callback' => '::settingsAjax',
+ 'wrapper' => 'field-layout-settings-wrapper',
+ ],
+ ];
+
+ $form['field_layouts']['settings_wrapper'] = [
+ '#type' => 'container',
+ '#id' => 'field-layout-settings-wrapper',
+ '#tree' => TRUE,
+ ];
+
+ if ($layout_plugin instanceof PluginFormInterface) {
+ $form['field_layouts']['settings_wrapper']['layout_settings'] = [];
+ $subform_state = SubformState::createForSubform($form['field_layouts']['settings_wrapper']['layout_settings'], $form, $form_state);
+ $form['field_layouts']['settings_wrapper']['layout_settings'] = $layout_plugin->buildConfigurationForm($form['field_layouts']['settings_wrapper']['layout_settings'], $subform_state);
+ }
+
+ return $form;
+ }
+
+ /**
+ * Gets the layout plugin for the currently selected field layout.
+ *
+ * @param \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface $entity
+ * The current form entity.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ *
+ * @return \Drupal\layout_plugin\Plugin\Layout\LayoutInterface
+ * The layout plugin.
+ */
+ protected function getLayoutPlugin(EntityDisplayWithLayoutInterface $entity, FormStateInterface $form_state) {
+ if (!$layout_plugin = $form_state->get('layout_plugin')) {
+ $stored_layout_id = $entity->getLayoutId();
+ // If a new field layout was selected, use that. Otherwise try to use the
+ // stored layout. Finally, fall back to the one column layout.
+ $layout_id = $form_state->getValue('field_layout') ?: ($stored_layout_id ?: 'onecol');
+ // If the current layout is the stored layout, use the stored layout
+ // settings. Otherwise leave the settings empty.
+ $layout_settings = $layout_id === $stored_layout_id ? $entity->getLayoutSettings() : [];
+
+ $layout_plugin = $this->layoutPluginManager->createInstance($layout_id, $layout_settings);
+ $form_state->set('layout_plugin', $layout_plugin);
+ }
+ return $layout_plugin;
+ }
+
+ /**
+ * Ajax callback for the field layout settings form.
+ */
+ public static function settingsAjax($form, FormStateInterface $form_state) {
+ return $form['field_layouts']['settings_wrapper'];
+ }
+
+ /**
+ * Submit handler for the non-JS case.
+ */
+ public static function settingsAjaxSubmit($form, FormStateInterface $form_state) {
+ $form_state->set('layout_plugin', NULL);
+ $form_state->setRebuild();
+ }
+
+ /**
+ * Overrides \Drupal\field_ui\Form\EntityDisplayFormBase::validateForm().
+ */
+ public function validateForm(array &$form, FormStateInterface $form_state) {
+ parent::validateForm($form, $form_state);
+
+ $layout_plugin = $this->getLayoutPlugin($this->getEntity(), $form_state);
+ if ($layout_plugin instanceof PluginFormInterface) {
+ $subform_state = SubformState::createForSubform($form['field_layouts']['settings_wrapper']['layout_settings'], $form, $form_state);
+ $layout_plugin->validateConfigurationForm($form['field_layouts']['settings_wrapper']['layout_settings'], $subform_state);
+ }
+ }
+
+ /**
+ * Overrides \Drupal\field_ui\Form\EntityDisplayFormBase::submitForm().
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ parent::submitForm($form, $form_state);
+
+ $entity = $this->getEntity();
+ $old_layout = $entity->getLayoutId();
+ $new_layout = $form_state->getValue('field_layout');
+
+ $layout_plugin = $this->getLayoutPlugin($entity, $form_state);
+ if ($layout_plugin instanceof PluginFormInterface) {
+ $layout_plugin->submitConfigurationForm($form['field_layouts']['settings_wrapper']['layout_settings'], SubformState::createForSubform($form['field_layouts']['settings_wrapper']['layout_settings'], $form, $form_state));
+ }
+ $entity->setLayout($new_layout, $layout_plugin->getConfiguration());
+
+ // If the layout is changing, reset all fields.
+ if ($new_layout !== $old_layout) {
+ // @todo Devise a mechanism for mapping old regions to new ones in
+ // https://www.drupal.org/node/2796877.
+ $new_region = $entity->getDefaultRegion();
+ foreach ($form_state->getValue('fields') as $field_name => $values) {
+ if (($component = $entity->getComponent($field_name)) && $new_region !== 'hidden') {
+ $component['region'] = $new_region;
+ $entity->setComponent($field_name, $component);
+ }
+ else {
+ $entity->removeComponent($field_name);
+ }
+ }
+ }
+ }
+
+ /**
+ * Gets the form entity.
+ *
+ * @return \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface
+ * The current form entity.
+ */
+ abstract public function getEntity();
+
+}
diff --git a/core/modules/field_layout/src/Form/FieldLayoutEntityFormDisplayEditForm.php b/core/modules/field_layout/src/Form/FieldLayoutEntityFormDisplayEditForm.php
new file mode 100644
index 0000000..20719c3
--- /dev/null
+++ b/core/modules/field_layout/src/Form/FieldLayoutEntityFormDisplayEditForm.php
@@ -0,0 +1,46 @@
+layoutPluginManager = $field_layout_plugin_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('plugin.manager.field.field_type'),
+ $container->get('plugin.manager.field.widget'),
+ $container->get('plugin.manager.layout_plugin')
+ );
+ }
+
+}
diff --git a/core/modules/field_layout/src/Form/FieldLayoutEntityViewDisplayEditForm.php b/core/modules/field_layout/src/Form/FieldLayoutEntityViewDisplayEditForm.php
new file mode 100644
index 0000000..f3da650
--- /dev/null
+++ b/core/modules/field_layout/src/Form/FieldLayoutEntityViewDisplayEditForm.php
@@ -0,0 +1,46 @@
+layoutPluginManager = $field_layout_plugin_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('plugin.manager.field.field_type'),
+ $container->get('plugin.manager.field.formatter'),
+ $container->get('plugin.manager.layout_plugin')
+ );
+ }
+
+}
diff --git a/core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.info.yml b/core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.info.yml
new file mode 100644
index 0000000..4d699e4
--- /dev/null
+++ b/core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.info.yml
@@ -0,0 +1,8 @@
+name: 'Field Layout test'
+type: module
+description: 'Support module for Field Layout tests.'
+core: 8.x
+package: Testing
+version: VERSION
+dependencies:
+ - entity_test
diff --git a/core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.routing.yml b/core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.routing.yml
new file mode 100644
index 0000000..bcea288
--- /dev/null
+++ b/core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.routing.yml
@@ -0,0 +1,7 @@
+entity.entity_test.test_view_mode:
+ path: '/entity_test/{entity_test}/test'
+ defaults:
+ _entity_view: 'entity_test.test'
+ _title: 'Test test view mode'
+ requirements:
+ _entity_access: 'entity_test.view'
diff --git a/core/modules/field_layout/tests/src/Functional/FieldLayoutTest.php b/core/modules/field_layout/tests/src/Functional/FieldLayoutTest.php
new file mode 100644
index 0000000..35b4bdd
--- /dev/null
+++ b/core/modules/field_layout/tests/src/Functional/FieldLayoutTest.php
@@ -0,0 +1,76 @@
+createContentType([
+ 'type' => 'article',
+ ]);
+ $this->createNode([
+ 'type' => 'article',
+ 'title' => 'The node title',
+ 'body' => [[
+ 'value' => 'The node body',
+ ]],
+ ]);
+ $this->drupalLogin($this->drupalCreateUser([
+ 'access administration pages',
+ 'administer content types',
+ 'administer nodes',
+ 'administer node fields',
+ 'administer node display',
+ 'administer node form display',
+ 'view the administration theme',
+ ]));
+ }
+
+ /**
+ * Tests an entity type that has fields shown by default.
+ */
+ public function testNodeView() {
+ // By default, the one column layout is used.
+ $this->drupalGet('node/1');
+ $this->assertSession()->elementExists('css', '.field-layout--onecol');
+ $this->assertSession()->elementExists('css', '.field-layout-region--content .field--name-body');
+
+ $this->drupalGet('admin/structure/types/manage/article/display');
+ $this->assertEquals(['Content', 'Disabled'], $this->getRegionTitles());
+ $this->assertSession()->optionExists('fields[body][region]', 'content');
+ }
+
+ /**
+ * Gets the region titles on the page.
+ *
+ * @return string[]
+ * An array of region titles.
+ */
+ protected function getRegionTitles() {
+ $region_titles = [];
+ $region_title_elements = $this->getSession()->getPage()->findAll('css', '.region-title td');
+ /** @var \Behat\Mink\Element\NodeElement[] $region_title_elements */
+ foreach ($region_title_elements as $region_title_element) {
+ $region_titles[] = $region_title_element->getText();
+ }
+ return $region_titles;
+ }
+
+}
diff --git a/core/modules/field_layout/tests/src/FunctionalJavascript/FieldLayoutTest.php b/core/modules/field_layout/tests/src/FunctionalJavascript/FieldLayoutTest.php
new file mode 100644
index 0000000..b7d2646
--- /dev/null
+++ b/core/modules/field_layout/tests/src/FunctionalJavascript/FieldLayoutTest.php
@@ -0,0 +1,266 @@
+ 'The name for this entity',
+ 'field_test_text' => [[
+ 'value' => 'The field test text value',
+ ]],
+ ]);
+ $entity->save();
+ $this->drupalLogin($this->drupalCreateUser([
+ 'access administration pages',
+ 'view test entity',
+ 'administer entity_test content',
+ 'administer entity_test fields',
+ 'administer entity_test display',
+ 'administer entity_test form display',
+ 'view the administration theme',
+ ]));
+ }
+
+ /**
+ * Tests that layouts are unique per-view mode.
+ */
+ public function testEntityViewModes() {
+ // By default, the field is not visible.
+ $this->drupalGet('entity_test/1/test');
+ $this->assertSession()->elementNotExists('css', '.field-layout-region--content .field--name-field-test-text');
+ $this->drupalGet('entity_test/1');
+ $this->assertSession()->elementNotExists('css', '.field-layout-region--content .field--name-field-test-text');
+
+ // Change the layout for the "test" view mode. See
+ // core.entity_view_mode.entity_test.test.yml.
+ $this->drupalGet('entity_test/structure/entity_test/display');
+ $this->click('#edit-modes');
+ $this->getSession()->getPage()->checkField('display_modes_custom[test]');
+ $this->submitForm([], 'Save');
+ $this->clickLink('configure them');
+ $this->getSession()->getPage()->pressButton('Show row weights');
+ $this->getSession()->getPage()->selectFieldOption('fields[field_test_text][region]', 'content');
+ $this->assertSession()->assertWaitOnAjaxRequest();
+ $this->submitForm([], 'Save');
+
+ // Each view mode has a different layout.
+ $this->drupalGet('entity_test/1/test');
+ $this->assertSession()->elementExists('css', '.field-layout-region--content .field--name-field-test-text');
+ $this->drupalGet('entity_test/1');
+ $this->assertSession()->elementNotExists('css', '.field-layout-region--content .field--name-field-test-text');
+ }
+
+ /**
+ * Tests the use of field layout for entity form displays.
+ */
+ public function testEntityForm() {
+ // By default, the one column layout is used.
+ $this->drupalGet('entity_test/manage/1/edit');
+ $this->assertFieldInRegion('field_test_text[0][value]', 'content');
+
+ // The one column layout is in use.
+ $this->drupalGet('entity_test/structure/entity_test/form-display');
+ $this->assertEquals(['Content', 'Disabled'], $this->getRegionTitles());
+
+ // Switch the layout to two columns.
+ $this->click('#edit-field-layouts');
+ $this->getSession()->getPage()->selectFieldOption('field_layout', 'twocol');
+ $this->assertSession()->assertWaitOnAjaxRequest();
+ $this->submitForm([], 'Save');
+
+ // The field is moved to the default region for the new layout.
+ $this->assertSession()->pageTextContains('Your settings have been saved.');
+ $this->assertEquals(['Left', 'Right', 'Disabled'], $this->getRegionTitles());
+
+ $this->drupalGet('entity_test/manage/1/edit');
+ // No fields are visible, and the regions don't display when empty.
+ $this->assertFieldInRegion('field_test_text[0][value]', 'left');
+ $this->assertSession()->elementExists('css', '.field-layout-region--left .field--name-field-test-text');
+
+ // After a refresh the new regions are still there.
+ $this->drupalGet('entity_test/structure/entity_test/form-display');
+ $this->assertEquals(['Left', 'Right', 'Disabled'], $this->getRegionTitles());
+
+ // Drag the field to the right region.
+ $field_test_text_row = $this->getSession()->getPage()->find('css', '#field-test-text');
+ $right_region_row = $this->getSession()->getPage()->find('css', '.region-right-message');
+ $field_test_text_row->find('css', '.handle')->dragTo($right_region_row);
+ $this->assertSession()->assertWaitOnAjaxRequest();
+ $this->submitForm([], 'Save');
+ $this->assertSession()->pageTextContains('Your settings have been saved.');
+
+ // The new layout is used.
+ $this->drupalGet('entity_test/manage/1/edit');
+ $this->assertSession()->elementExists('css', '.field-layout-region--right .field--name-field-test-text');
+ $this->assertFieldInRegion('field_test_text[0][value]', 'right');
+
+ // Move the field to the right region without tabledrag.
+ $this->drupalGet('entity_test/structure/entity_test/form-display');
+ $this->getSession()->getPage()->pressButton('Show row weights');
+ $this->getSession()->getPage()->selectFieldOption('fields[field_test_text][region]', 'right');
+ $this->assertSession()->assertWaitOnAjaxRequest();
+ $this->submitForm([], 'Save');
+ $this->assertSession()->pageTextContains('Your settings have been saved.');
+
+ // The updated region is used.
+ $this->drupalGet('entity_test/manage/1/edit');
+ $this->assertFieldInRegion('field_test_text[0][value]', 'right');
+
+ // The layout is still in use without Field UI.
+ $this->container->get('module_installer')->uninstall(['field_ui']);
+ $this->drupalGet('entity_test/manage/1/edit');
+ $this->assertFieldInRegion('field_test_text[0][value]', 'right');
+ }
+
+ /**
+ * Tests the use of field layout for entity view displays.
+ */
+ public function testEntityView() {
+ // The one column layout is in use.
+ $this->drupalGet('entity_test/structure/entity_test/display');
+ $this->assertEquals(['Content', 'Disabled'], $this->getRegionTitles());
+
+ // Switch the layout to two columns.
+ $this->click('#edit-field-layouts');
+ $this->getSession()->getPage()->selectFieldOption('field_layout', 'twocol');
+ $this->assertSession()->assertWaitOnAjaxRequest();
+ $this->submitForm([], 'Save');
+
+ $this->assertSession()->pageTextContains('Your settings have been saved.');
+ $this->assertEquals(['Left', 'Right', 'Disabled'], $this->getRegionTitles());
+
+ $this->drupalGet('entity_test/1');
+ // No fields are visible, and the regions don't display when empty.
+ $this->assertSession()->elementNotExists('css', '.field-layout--twocol');
+ $this->assertSession()->elementNotExists('css', '.field-layout-region');
+ $this->assertSession()->elementNotExists('css', '.field--name-field-test-text');
+
+ // After a refresh the new regions are still there.
+ $this->drupalGet('entity_test/structure/entity_test/display');
+ $this->assertEquals(['Left', 'Right', 'Disabled'], $this->getRegionTitles());
+
+ // Drag the field to the left region.
+ $this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'hidden')->isSelected());
+ $field_test_text_row = $this->getSession()->getPage()->find('css', '#field-test-text');
+ $left_region_row = $this->getSession()->getPage()->find('css', '.region-left-message');
+ $field_test_text_row->find('css', '.handle')->dragTo($left_region_row);
+ $this->assertSession()->assertWaitOnAjaxRequest();
+ $this->assertFalse($this->assertSession()->optionExists('fields[field_test_text][region]', 'hidden')->isSelected());
+ $this->submitForm([], 'Save');
+ $this->assertSession()->pageTextContains('Your settings have been saved.');
+
+ // The new layout is used.
+ $this->drupalGet('entity_test/1');
+ $this->assertSession()->elementExists('css', '.field-layout--twocol');
+ $this->assertSession()->elementExists('css', '.field-layout-region--left .field--name-field-test-text');
+
+ // Move the field to the right region without tabledrag.
+ $this->drupalGet('entity_test/structure/entity_test/display');
+ $this->getSession()->getPage()->pressButton('Show row weights');
+ $this->getSession()->getPage()->selectFieldOption('fields[field_test_text][region]', 'right');
+ $this->assertSession()->assertWaitOnAjaxRequest();
+ $this->submitForm([], 'Save');
+ $this->assertSession()->pageTextContains('Your settings have been saved.');
+
+ // The updated region is used.
+ $this->drupalGet('entity_test/1');
+ $this->assertSession()->elementExists('css', '.field-layout-region--right .field--name-field-test-text');
+
+ // The layout is still in use without Field UI.
+ $this->container->get('module_installer')->uninstall(['field_ui']);
+ $this->drupalGet('entity_test/1');
+ $this->assertSession()->elementExists('css', '.field-layout--twocol');
+ $this->assertSession()->elementExists('css', '.field-layout-region--right .field--name-field-test-text');
+ }
+
+ /**
+ * Tests layout plugins with forms.
+ */
+ public function testLayoutForms() {
+ $this->drupalGet('entity_test/structure/entity_test/display');
+ // Switch to a field layout with settings.
+ $this->click('#edit-field-layouts');
+
+ // Test switching between layouts with and without forms.
+ $this->getSession()->getPage()->selectFieldOption('field_layout', 'layout_test_plugin');
+ $this->assertSession()->assertWaitOnAjaxRequest();
+ $this->assertSession()->fieldExists('settings_wrapper[layout_settings][setting_1]');
+
+ $this->getSession()->getPage()->selectFieldOption('field_layout', 'layout_test_2col');
+ $this->assertSession()->assertWaitOnAjaxRequest();
+ $this->assertSession()->fieldNotExists('settings_wrapper[layout_settings][setting_1]');
+
+ $this->getSession()->getPage()->selectFieldOption('field_layout', 'layout_test_plugin');
+ $this->assertSession()->assertWaitOnAjaxRequest();
+ $this->assertSession()->fieldExists('settings_wrapper[layout_settings][setting_1]');
+
+ // Move the test field to the content region.
+ $this->getSession()->getPage()->pressButton('Show row weights');
+ $this->getSession()->getPage()->selectFieldOption('fields[field_test_text][region]', 'content');
+ $this->assertSession()->assertWaitOnAjaxRequest();
+ $this->submitForm([], 'Save');
+
+ $this->drupalGet('entity_test/1');
+ $this->assertSession()->pageTextContains('Blah: Default');
+
+ // Update the field layout settings.
+ $this->drupalGet('entity_test/structure/entity_test/display');
+ $this->click('#edit-field-layouts');
+ $this->getSession()->getPage()->fillField('settings_wrapper[layout_settings][setting_1]', 'Test text');
+ $this->submitForm([], 'Save');
+
+ $this->drupalGet('entity_test/1');
+ $this->assertSession()->pageTextContains('Blah: Test text');
+ }
+
+ /**
+ * Gets the region titles on the page.
+ *
+ * @return string[]
+ * An array of region titles.
+ */
+ protected function getRegionTitles() {
+ $region_titles = [];
+ $region_title_elements = $this->getSession()->getPage()->findAll('css', '.region-title td');
+ /** @var \Behat\Mink\Element\NodeElement[] $region_title_elements */
+ foreach ($region_title_elements as $region_title_element) {
+ $region_titles[] = $region_title_element->getText();
+ }
+ return $region_titles;
+ }
+
+ /**
+ * Asserts that a field exists in a given region.
+ *
+ * @param string $field_selector
+ * The field selector, one of field id|name|label|value.
+ * @param string $region_name
+ * The machine name of the region.
+ */
+ protected function assertFieldInRegion($field_selector, $region_name) {
+ $region_element = $this->getSession()->getPage()->find('css', ".field-layout-region--$region_name");
+ $this->assertNotNull($region_element);
+ $this->assertSession()->fieldExists($field_selector, $region_element);
+ }
+
+}
diff --git a/core/modules/field_layout/tests/src/Unit/FieldLayoutBuilderTest.php b/core/modules/field_layout/tests/src/Unit/FieldLayoutBuilderTest.php
new file mode 100644
index 0000000..6e4f698
--- /dev/null
+++ b/core/modules/field_layout/tests/src/Unit/FieldLayoutBuilderTest.php
@@ -0,0 +1,283 @@
+pluginDefinition = new LayoutDefinition([
+ 'library' => 'field_layout/drupal.field_layout.twocol',
+ 'theme_hook' => 'field_layout__twocol',
+ 'regions' => [
+ 'left' => [
+ 'label' => 'Left',
+ ],
+ 'right' => [
+ 'label' => 'Right',
+ ],
+ ],
+ ]);
+ $this->layoutPlugin = new LayoutDefault([], 'twocol', $this->pluginDefinition);
+
+ $this->layoutPluginManager = $this->prophesize(LayoutPluginManagerInterface::class);
+ $this->layoutPluginManager->getDefinition('unknown', FALSE)->willReturn(NULL);
+ $this->layoutPluginManager->getDefinition('twocol', FALSE)->willReturn($this->pluginDefinition);
+
+ $this->entityFieldManager = $this->prophesize(EntityFieldManagerInterface::class);
+
+ $this->fieldLayoutBuilder = new FieldLayoutBuilder($this->layoutPluginManager->reveal(), $this->entityFieldManager->reveal());
+ }
+
+ /**
+ * @covers ::build
+ * @covers ::getFields
+ */
+ public function testBuildView() {
+ $definitions = [];
+ $non_configurable_field_definition = $this->prophesize(FieldDefinitionInterface::class);
+ $non_configurable_field_definition->isDisplayConfigurable('view')->willReturn(FALSE);
+ $definitions['non_configurable_field'] = $non_configurable_field_definition->reveal();
+ $this->entityFieldManager->getFieldDefinitions('the_entity_type_id', 'the_entity_type_bundle')->willReturn($definitions);
+
+ $build = [
+ 'test1' => [
+ '#markup' => 'Test1',
+ ],
+ 'non_configurable_field' => [
+ '#markup' => 'Non-configurable',
+ ],
+ ];
+
+ $display = $this->prophesize(EntityDisplayWithLayoutInterface::class);
+ $display->getTargetEntityTypeId()->willReturn('the_entity_type_id');
+ $display->getTargetBundle()->willReturn('the_entity_type_bundle');
+ $display->getLayoutPlugin()->willReturn($this->layoutPlugin);
+ $display->getLayoutId()->willReturn('twocol');
+ $display->getLayoutSettings()->willReturn([]);
+ $display->getComponents()->willReturn([
+ 'test1' => [
+ 'region' => 'right',
+ ],
+ 'non_configurable_field' => [
+ 'region' => 'left',
+ ],
+ ]);
+
+ $display_context = 'view';
+
+ $expected = [
+ 'non_configurable_field' => [
+ '#markup' => 'Non-configurable',
+ ],
+ 'field_layout' => [
+ '#content' => [
+ 'left' => [],
+ 'right' => [
+ 'test1' => [
+ '#markup' => 'Test1',
+ ],
+ ],
+ ],
+ '#settings' => [],
+ '#layout' => $this->pluginDefinition,
+ '#theme' => 'field_layout__twocol',
+ '#attached' => [
+ 'library' => [
+ 'field_layout/drupal.field_layout.twocol',
+ ],
+ ],
+ '#process' => [['Drupal\field_layout\FieldLayoutBuilder', 'processContent']],
+ ],
+ ];
+ $this->fieldLayoutBuilder->build($build, $display->reveal(), $display_context);
+ $this->assertEquals($expected, $build);
+ $this->assertSame($expected, $build);
+ }
+
+ /**
+ * @covers ::build
+ * @covers ::getFields
+ */
+ public function testBuildForm() {
+ $definitions = [];
+ $non_configurable_field_definition = $this->prophesize(FieldDefinitionInterface::class);
+ $non_configurable_field_definition->isDisplayConfigurable('form')->willReturn(FALSE);
+ $definitions['non_configurable_field'] = $non_configurable_field_definition->reveal();
+ $this->entityFieldManager->getFieldDefinitions('the_entity_type_id', 'the_entity_type_bundle')->willReturn($definitions);
+
+ $build = [
+ 'test1' => [
+ '#markup' => 'Test1',
+ ],
+ 'non_configurable_field' => [
+ '#markup' => 'Non-configurable',
+ ],
+ ];
+
+ $display = $this->prophesize(EntityDisplayWithLayoutInterface::class);
+ $display->getTargetEntityTypeId()->willReturn('the_entity_type_id');
+ $display->getTargetBundle()->willReturn('the_entity_type_bundle');
+ $display->getLayoutPlugin()->willReturn($this->layoutPlugin);
+ $display->getLayoutId()->willReturn('twocol');
+ $display->getLayoutSettings()->willReturn([]);
+ $display->getComponents()->willReturn([
+ 'test1' => [
+ 'region' => 'right',
+ ],
+ 'non_configurable_field' => [
+ 'region' => 'left',
+ ],
+ ]);
+
+ $display_context = 'form';
+
+ $expected = [
+ 'test1' => [
+ '#markup' => 'Test1',
+ '#group' => 'right',
+ ],
+ 'non_configurable_field' => [
+ '#markup' => 'Non-configurable',
+ ],
+ 'field_layout' => [
+ '#content' => [
+ 'left' => [
+ '#process' => ['\Drupal\Core\Render\Element\RenderElement::processGroup'],
+ '#pre_render' => ['\Drupal\Core\Render\Element\RenderElement::preRenderGroup'],
+ ],
+ 'right' => [
+ '#process' => ['\Drupal\Core\Render\Element\RenderElement::processGroup'],
+ '#pre_render' => ['\Drupal\Core\Render\Element\RenderElement::preRenderGroup'],
+ ],
+ ],
+ '#settings' => [],
+ '#layout' => $this->pluginDefinition,
+ '#theme' => 'field_layout__twocol',
+ '#attached' => [
+ 'library' => [
+ 'field_layout/drupal.field_layout.twocol',
+ ],
+ ],
+ '#process' => [['Drupal\field_layout\FieldLayoutBuilder', 'processContent']],
+ ],
+ ];
+ $this->fieldLayoutBuilder->build($build, $display->reveal(), $display_context);
+ $this->assertEquals($expected, $build);
+ $this->assertSame($expected, $build);
+ }
+
+ /**
+ * @covers ::build
+ */
+ public function testBuildEmpty() {
+ $definitions = [];
+ $non_configurable_field_definition = $this->prophesize(FieldDefinitionInterface::class);
+ $non_configurable_field_definition->isDisplayConfigurable('form')->willReturn(FALSE);
+ $definitions['non_configurable_field'] = $non_configurable_field_definition->reveal();
+ $this->entityFieldManager->getFieldDefinitions('the_entity_type_id', 'the_entity_type_bundle')->willReturn($definitions);
+
+ $build = [
+ 'non_configurable_field' => [
+ '#markup' => 'Non-configurable',
+ ],
+ ];
+
+ $display = $this->prophesize(EntityDisplayWithLayoutInterface::class);
+ $display->getTargetEntityTypeId()->willReturn('the_entity_type_id');
+ $display->getTargetBundle()->willReturn('the_entity_type_bundle');
+ $display->getLayoutPlugin()->willReturn($this->layoutPlugin);
+ $display->getLayoutId()->willReturn('twocol');
+ $display->getLayoutSettings()->willReturn([]);
+ $display->getComponents()->willReturn([
+ 'test1' => [
+ 'region' => 'right',
+ ],
+ 'non_configurable_field' => [
+ 'region' => 'left',
+ ],
+ ]);
+
+ $display_context = 'form';
+
+ $expected = [
+ 'non_configurable_field' => [
+ '#markup' => 'Non-configurable',
+ ],
+ ];
+ $this->fieldLayoutBuilder->build($build, $display->reveal(), $display_context);
+ $this->assertSame($expected, $build);
+ }
+
+ /**
+ * @covers ::build
+ */
+ public function testBuildNoLayout() {
+ $this->entityFieldManager->getFieldDefinitions(Argument::any(), Argument::any())->shouldNotBeCalled();
+
+ $build = [
+ 'test1' => [
+ '#markup' => 'Test1',
+ ],
+ ];
+
+ $display = $this->prophesize(EntityDisplayWithLayoutInterface::class);
+ $display->getLayoutId()->willReturn('unknown');
+ $display->getLayoutSettings()->willReturn([]);
+ $display->getComponents()->shouldNotBeCalled();
+
+ $display_context = 'form';
+
+ $expected = [
+ 'test1' => [
+ '#markup' => 'Test1',
+ ],
+ ];
+ $this->fieldLayoutBuilder->build($build, $display->reveal(), $display_context);
+ $this->assertSame($expected, $build);
+ }
+
+}
diff --git a/core/modules/field_ui/field_ui.js b/core/modules/field_ui/field_ui.js
index a53553d..81761ed 100644
--- a/core/modules/field_ui/field_ui.js
+++ b/core/modules/field_ui/field_ui.js
@@ -265,11 +265,16 @@ else if ($this.is('.region-empty')) {
this.name = data.name;
this.region = data.region;
this.tableDrag = data.tableDrag;
+ this.defaultPlugin = data.defaultPlugin;
// Attach change listener to the 'plugin type' select.
this.$pluginSelect = $(row).find('select.field-plugin-type');
this.$pluginSelect.on('change', Drupal.fieldUIOverview.onChange);
+ // Attach change listener to the 'region' select.
+ this.$regionSelect = $(row).find('select.field-region');
+ this.$regionSelect.on('change', Drupal.fieldUIOverview.onChange);
+
return this;
};
@@ -282,7 +287,7 @@ else if ($this.is('.region-empty')) {
* Either 'hidden' or 'content'.
*/
getRegion: function () {
- return (this.$pluginSelect.val() === 'hidden') ? 'hidden' : 'content';
+ return this.$regionSelect.val();
},
/**
@@ -305,24 +310,16 @@ else if ($this.is('.region-empty')) {
* {@link Drupal.fieldUIOverview.AJAXRefreshRows}.
*/
regionChange: function (region) {
+ // Replace dashes with underscores.
+ region = region.replace(/-/g, '_');
- // When triggered by a row drag, the 'format' select needs to be adjusted
- // to the new region.
- var currentValue = this.$pluginSelect.val();
- var value;
- // @TODO Check if this couldn't just be like
- // if (region !== 'hidden') {
- if (region === 'content') {
- if (currentValue === 'hidden') {
- // Restore the formatter back to the default formatter. Pseudo-fields
- // do not have default formatters, we just return to 'visible' for
- // those.
- value = (typeof this.defaultPlugin !== 'undefined') ? this.defaultPlugin : this.$pluginSelect.find('option').val();
- }
- }
- else {
- value = 'hidden';
- }
+ // Set the region of the select list.
+ this.$regionSelect.val(region);
+
+ // Restore the formatter back to the default formatter. Pseudo-fields
+ // do not have default formatters, we just return to 'visible' for
+ // those.
+ var value = (typeof this.defaultPlugin !== 'undefined') ? this.defaultPlugin : this.$pluginSelect.find('option').val();
if (typeof value !== 'undefined') {
this.$pluginSelect.val(value);
diff --git a/core/modules/field_ui/src/Form/EntityDisplayFormBase.php b/core/modules/field_ui/src/Form/EntityDisplayFormBase.php
index 586f1ef..4b9a85d 100644
--- a/core/modules/field_ui/src/Form/EntityDisplayFormBase.php
+++ b/core/modules/field_ui/src/Form/EntityDisplayFormBase.php
@@ -172,6 +172,13 @@ public function form(array $form, FormStateInterface $form_state) {
'subgroup' => 'field-parent',
'source' => 'field-name',
),
+ array(
+ 'action' => 'match',
+ 'relationship' => 'parent',
+ 'group' => 'field-region',
+ 'subgroup' => 'field-region',
+ 'source' => 'field-name',
+ ),
),
);
@@ -309,6 +316,14 @@ protected function buildFieldRow(FieldDefinitionInterface $field_definition, arr
'#attributes' => array('class' => array('field-name')),
),
),
+ 'region' => array(
+ '#type' => 'select',
+ '#title' => $this->t('Region for @title', array('@title' => $label)),
+ '#title_display' => 'invisible',
+ '#options' => $this->getRegionOptions(),
+ '#default_value' => $display_options ? $display_options['region'] : 'hidden',
+ '#attributes' => array('class' => array('field-region')),
+ ),
);
$field_row['plugin'] = array(
@@ -316,7 +331,7 @@ protected function buildFieldRow(FieldDefinitionInterface $field_definition, arr
'#type' => 'select',
'#title' => $this->t('Plugin for @title', array('@title' => $label)),
'#title_display' => 'invisible',
- '#options' => $this->getPluginOptions($field_definition),
+ '#options' => $this->getApplicablePluginOptions($field_definition),
'#default_value' => $display_options ? $display_options['type'] : 'hidden',
'#parents' => array('fields', $field_name, 'type'),
'#attributes' => array('class' => array('field-plugin-type')),
@@ -474,6 +489,14 @@ protected function buildExtraFieldRow($field_id, $extra_field) {
'#attributes' => array('class' => array('field-name')),
),
),
+ 'region' => array(
+ '#type' => 'select',
+ '#title' => $this->t('Region for @title', array('@title' => $extra_field['label'])),
+ '#title_display' => 'invisible',
+ '#options' => $this->getRegionOptions(),
+ '#default_value' => $display_options ? $display_options['region'] : 'hidden',
+ '#attributes' => array('class' => array('field-region')),
+ ),
'plugin' => array(
'type' => array(
'#type' => 'select',
@@ -550,7 +573,7 @@ protected function copyFormValuesToEntity(EntityInterface $entity, array $form,
foreach ($form['#fields'] as $field_name) {
$values = $form_values['fields'][$field_name];
- if ($values['type'] == 'hidden') {
+ if ($values['region'] == 'hidden') {
$entity->removeComponent($field_name);
}
else {
@@ -567,6 +590,7 @@ protected function copyFormValuesToEntity(EntityInterface $entity, array $form,
$options['type'] = $values['type'];
$options['weight'] = $values['weight'];
+ $options['region'] = $values['region'];
// Only formatters have configurable label visibility.
if (isset($values['label'])) {
$options['label'] = $values['label'];
@@ -577,12 +601,13 @@ protected function copyFormValuesToEntity(EntityInterface $entity, array $form,
// Collect data for 'extra' fields.
foreach ($form['#extra'] as $name) {
- if ($form_values['fields'][$name]['type'] == 'hidden') {
+ if ($form_values['fields'][$name]['region'] == 'hidden') {
$entity->removeComponent($name);
}
else {
$entity->setComponent($name, array(
'weight' => $form_values['fields'][$name]['weight'],
+ 'region' => $form_values['fields'][$name]['region'],
));
}
}
@@ -752,20 +777,6 @@ protected function getApplicablePluginOptions(FieldDefinitionInterface $field_de
}
/**
- * Returns an array of widget or formatter options for a field.
- *
- * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
- * The field definition.
- *
- * @return array
- * An array of widget or formatter options.
- */
- protected function getPluginOptions(FieldDefinitionInterface $field_definition) {
- $applicable_options = $this->getApplicablePluginOptions($field_definition);
- return $applicable_options + array('hidden' => '- ' . $this->t('Hidden') . ' -');
- }
-
- /**
* Returns the ID of the default widget or formatter plugin for a field type.
*
* @param string $field_type
@@ -813,7 +824,7 @@ public function getRowRegion($row) {
switch ($row['#row_type']) {
case 'field':
case 'extra_field':
- return ($row['plugin']['type']['#value'] == 'hidden' ? 'hidden' : 'content');
+ return $row['region']['#value'] ?: 'hidden';
}
}
@@ -826,7 +837,6 @@ public function getRowRegion($row) {
protected function getExtraFieldVisibilityOptions() {
return array(
'visible' => $this->t('Visible'),
- 'hidden' => '- ' . $this->t('Hidden') . ' -',
);
}
diff --git a/core/modules/field_ui/src/Form/EntityFormDisplayEditForm.php b/core/modules/field_ui/src/Form/EntityFormDisplayEditForm.php
index 741b98d..af8e2ed 100644
--- a/core/modules/field_ui/src/Form/EntityFormDisplayEditForm.php
+++ b/core/modules/field_ui/src/Form/EntityFormDisplayEditForm.php
@@ -94,6 +94,7 @@ protected function getTableHeader() {
$this->t('Field'),
$this->t('Weight'),
$this->t('Parent'),
+ $this->t('Region'),
array('data' => $this->t('Widget'), 'colspan' => 3),
);
}
diff --git a/core/modules/field_ui/src/Form/EntityViewDisplayEditForm.php b/core/modules/field_ui/src/Form/EntityViewDisplayEditForm.php
index f273325..174726f 100644
--- a/core/modules/field_ui/src/Form/EntityViewDisplayEditForm.php
+++ b/core/modules/field_ui/src/Form/EntityViewDisplayEditForm.php
@@ -127,6 +127,7 @@ protected function getTableHeader() {
$this->t('Field'),
$this->t('Weight'),
$this->t('Parent'),
+ $this->t('Region'),
$this->t('Label'),
array('data' => $this->t('Format'), 'colspan' => 3),
);
diff --git a/core/modules/field_ui/src/Tests/ManageDisplayTest.php b/core/modules/field_ui/src/Tests/ManageDisplayTest.php
index 4cd2901..902a193 100644
--- a/core/modules/field_ui/src/Tests/ManageDisplayTest.php
+++ b/core/modules/field_ui/src/Tests/ManageDisplayTest.php
@@ -3,6 +3,7 @@
namespace Drupal\field_ui\Tests;
use Drupal\Component\Utility\Unicode;
+use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Language\LanguageInterface;
@@ -94,12 +95,31 @@ function testFormatterUI() {
'field_test_multiple',
'field_test_with_prepare_view',
'field_test_applicable',
- 'hidden',
);
$this->assertEqual($options, $expected_options, 'The expected formatter ordering is respected.');
+ // Ensure that fields can be hidden directly by changing the region.
+ $this->drupalGet($manage_display);
+ $this->assertFieldByName('fields[field_test][region]', 'content');
+ $edit = ['fields[field_test][region]' => 'hidden'];
+ $this->drupalPostForm($manage_display, $edit, t('Save'));
+ $this->assertFieldByName('fields[field_test][region]', 'hidden');
+ $display = EntityViewDisplay::load("node.{$this->type}.default");
+ $this->assertNull($display->getComponent('field_test'));
+
+ // Restore the field to the content region.
+ $edit = [
+ 'fields[field_test][type]' => 'field_test_default',
+ 'fields[field_test][region]' => 'content',
+ ];
+ $this->drupalPostForm($manage_display, $edit, t('Save'));
+
// Change the formatter and check that the summary is updated.
- $edit = array('fields[field_test][type]' => 'field_test_multiple', 'refresh_rows' => 'field_test');
+ $edit = array(
+ 'fields[field_test][type]' => 'field_test_multiple',
+ 'fields[field_test][region]' => 'content',
+ 'refresh_rows' => 'field_test'
+ );
$this->drupalPostAjaxForm(NULL, $edit, array('op' => t('Refresh')));
$format = 'field_test_multiple';
$default_settings = \Drupal::service('plugin.manager.field.formatter')->getDefaultSettings($format);
@@ -147,7 +167,10 @@ function testFormatterUI() {
$this->assertFieldByName($fieldname, '');
// Test the empty setting formatter.
- $edit = array('fields[field_test][type]' => 'field_empty_setting');
+ $edit = array(
+ 'fields[field_test][type]' => 'field_empty_setting',
+ 'fields[field_test][region]' => 'content',
+ );
$this->drupalPostForm(NULL, $edit, t('Save'));
$this->assertNoText('Default empty setting now has a value.');
$this->assertFieldById('edit-fields-field-test-settings-edit');
@@ -159,7 +182,11 @@ function testFormatterUI() {
// Test the settings form behavior. An edit button should be present since
// there are third party settings to configure.
- $edit = array('fields[field_test][type]' => 'field_no_settings', 'refresh_rows' => 'field_test');
+ $edit = array(
+ 'fields[field_test][type]' => 'field_no_settings',
+ 'fields[field_test][region]' => 'content',
+ 'refresh_rows' => 'field_test',
+ );
$this->drupalPostAjaxForm(NULL, $edit, array('op' => t('Refresh')));
$this->assertFieldByName('field_test_settings_edit');
@@ -225,12 +252,15 @@ public function testWidgetUI() {
$expected_options = array (
'test_field_widget',
'test_field_widget_multiple',
- 'hidden',
);
$this->assertEqual($options, $expected_options, 'The expected widget ordering is respected.');
// Change the widget and check that the summary is updated.
- $edit = array('fields[field_test][type]' => 'test_field_widget_multiple', 'refresh_rows' => 'field_test');
+ $edit = array(
+ 'fields[field_test][type]' => 'test_field_widget_multiple',
+ 'fields[field_test][region]' => 'content',
+ 'refresh_rows' => 'field_test',
+ );
$this->drupalPostAjaxForm(NULL, $edit, array('op' => t('Refresh')));
$widget_type = 'test_field_widget_multiple';
$default_settings = \Drupal::service('plugin.manager.field.widget')->getDefaultSettings($widget_type);
@@ -282,8 +312,16 @@ public function testWidgetUI() {
$this->drupalGet($manage_display);
// Checks if the select elements contain the specified options.
- $this->assertFieldSelectOptions('fields[field_test][type]', array('test_field_widget', 'test_field_widget_multiple', 'hidden'));
- $this->assertFieldSelectOptions('fields[field_onewidgetfield][type]', array('test_field_widget', 'hidden'));
+ $this->assertFieldSelectOptions('fields[field_test][type]', array('test_field_widget', 'test_field_widget_multiple'));
+ $this->assertFieldSelectOptions('fields[field_onewidgetfield][type]', array('test_field_widget'));
+
+ // Ensure that fields can be hidden directly by changing the region.
+ $this->assertFieldByName('fields[field_test][region]', 'content');
+ $edit = ['fields[field_test][region]' => 'hidden'];
+ $this->drupalPostForm(NULL, $edit, t('Save'));
+ $this->assertFieldByName('fields[field_test][region]', 'hidden');
+ $display = EntityFormDisplay::load("node.{$this->type}.default");
+ $this->assertNull($display->getComponent('field_test'));
}
/**
@@ -321,6 +359,7 @@ function testViewModeCustom() {
// accordingly in 'rss' mode.
$edit = array(
'fields[field_test][type]' => 'field_test_with_prepare_view',
+ 'fields[field_test][region]' => 'content',
);
$this->drupalPostForm('admin/structure/types/manage/' . $this->type . '/display', $edit, t('Save'));
$this->assertNodeViewText($node, 'rss', $output['field_test_with_prepare_view'], "The field is displayed as expected in view modes that use 'default' settings.");
@@ -335,7 +374,7 @@ function testViewModeCustom() {
// Set the field to 'hidden' in the view mode, check that the field is
// hidden.
$edit = array(
- 'fields[field_test][type]' => 'hidden',
+ 'fields[field_test][region]' => 'hidden',
);
$this->drupalPostForm('admin/structure/types/manage/' . $this->type . '/display/rss', $edit, t('Save'));
$this->assertNodeViewNoText($node, 'rss', $value, "The field is hidden in 'rss' mode.");
@@ -378,7 +417,7 @@ function testNonInitializedFields() {
// Check that the field appears as 'hidden' on the 'Manage display' page
// for the 'teaser' mode.
$this->drupalGet('admin/structure/types/manage/' . $this->type . '/display/teaser');
- $this->assertFieldByName('fields[field_test][type]', 'hidden', 'The field is displayed as \'hidden \'.');
+ $this->assertFieldByName('fields[field_test][region]', 'hidden', 'The field is displayed as \'hidden \'.');
}
/**
diff --git a/core/modules/field_ui/tests/src/Functional/EntityDisplayTest.php b/core/modules/field_ui/tests/src/Functional/EntityDisplayTest.php
new file mode 100644
index 0000000..c9cfc05
--- /dev/null
+++ b/core/modules/field_ui/tests/src/Functional/EntityDisplayTest.php
@@ -0,0 +1,46 @@
+drupalLogin($this->drupalCreateUser([
+ 'administer entity_test display',
+ ]));
+ }
+
+ /**
+ * Tests the use of regions for entity view displays.
+ */
+ public function testEntityView() {
+ $this->drupalGet('entity_test/structure/entity_test/display');
+ $this->assertSession()->elementExists('css', '.region-content-message.region-empty');
+ $this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'hidden')->isSelected());
+
+ $this->getSession()->getPage()->selectFieldOption('fields[field_test_text][region]', 'content');
+ $this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'content')->isSelected());
+
+ $this->submitForm([], 'Save');
+ $this->assertSession()->pageTextContains('Your settings have been saved.');
+ $this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'content')->isSelected());
+ }
+
+}
diff --git a/core/modules/field_ui/tests/src/FunctionalJavascript/EntityDisplayTest.php b/core/modules/field_ui/tests/src/FunctionalJavascript/EntityDisplayTest.php
new file mode 100644
index 0000000..319e8df
--- /dev/null
+++ b/core/modules/field_ui/tests/src/FunctionalJavascript/EntityDisplayTest.php
@@ -0,0 +1,89 @@
+ 'The name for this entity',
+ 'field_test_text' => [[
+ 'value' => 'The field test text value',
+ ]],
+ ]);
+ $entity->save();
+ $this->drupalLogin($this->drupalCreateUser([
+ 'access administration pages',
+ 'view test entity',
+ 'administer entity_test content',
+ 'administer entity_test fields',
+ 'administer entity_test display',
+ 'administer entity_test form display',
+ 'view the administration theme',
+ ]));
+ }
+
+ /**
+ * Tests the use of regions for entity form displays.
+ */
+ public function testEntityForm() {
+ $this->drupalGet('entity_test/manage/1/edit');
+ $this->assertSession()->fieldExists('field_test_text[0][value]');
+
+ $this->drupalGet('entity_test/structure/entity_test/form-display');
+ $this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'content')->isSelected());
+
+ $this->getSession()->getPage()->selectFieldOption('fields[field_test_text][region]', 'hidden');
+ $this->assertSession()->assertWaitOnAjaxRequest();
+ $this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'hidden')->isSelected());
+
+ $this->submitForm([], 'Save');
+ $this->assertSession()->pageTextContains('Your settings have been saved.');
+ $this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'hidden')->isSelected());
+
+ $this->drupalGet('entity_test/manage/1/edit');
+ $this->assertSession()->fieldNotExists('field_test_text[0][value]');
+ }
+
+ /**
+ * Tests the use of regions for entity view displays.
+ */
+ public function testEntityView() {
+ $this->drupalGet('entity_test/1');
+ $this->assertSession()->elementNotExists('css', '.field--name-field-test-text');
+
+ $this->drupalGet('entity_test/structure/entity_test/display');
+ $this->assertSession()->elementExists('css', '.region-content-message.region-empty');
+ $this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'hidden')->isSelected());
+
+ $this->getSession()->getPage()->selectFieldOption('fields[field_test_text][region]', 'content');
+ $this->assertSession()->assertWaitOnAjaxRequest();
+ $this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'content')->isSelected());
+
+ $this->submitForm([], 'Save');
+ $this->assertSession()->pageTextContains('Your settings have been saved.');
+ $this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'content')->isSelected());
+
+ $this->drupalGet('entity_test/1');
+ $this->assertSession()->elementExists('css', '.field--name-field-test-text');
+ }
+
+}
diff --git a/core/modules/field_ui/tests/src/Kernel/EntityDisplayTest.php b/core/modules/field_ui/tests/src/Kernel/EntityDisplayTest.php
index be188db..d060163 100644
--- a/core/modules/field_ui/tests/src/Kernel/EntityDisplayTest.php
+++ b/core/modules/field_ui/tests/src/Kernel/EntityDisplayTest.php
@@ -69,6 +69,7 @@ public function testEntityDisplayCRUD() {
$display->save();
$display = EntityViewDisplay::load($display->id());
foreach (array('component_1', 'component_2', 'component_3') as $name) {
+ $expected[$name]['region'] = 'content';
$this->assertEqual($display->getComponent($name), $expected[$name]);
}
@@ -86,6 +87,7 @@ public function testEntityDisplayCRUD() {
'link_to_entity' => FALSE,
),
'third_party_settings' => array(),
+ 'region' => 'content'
);
$this->assertEqual($display->getComponents(), $expected);
@@ -148,7 +150,7 @@ public function testEntityGetDisplay() {
$display = entity_get_display('entity_test', 'entity_test', 'default');
$this->assertFalse($display->isNew());
$this->assertEqual($display->id(), 'entity_test.entity_test.default');
- $this->assertEqual($display->getComponent('component_1'), array( 'weight' => 10, 'settings' => array(), 'third_party_settings' => array()));
+ $this->assertEqual($display->getComponent('component_1'), array( 'weight' => 10, 'settings' => array(), 'third_party_settings' => array(), 'region' => 'content'));
}
/**
@@ -164,7 +166,36 @@ public function testExtraFieldComponent() {
// Check that the default visibility taken into account for extra fields
// unknown in the display.
- $this->assertEqual($display->getComponent('display_extra_field'), array('weight' => 5));
+ $this->assertEqual($display->getComponent('display_extra_field'), array('weight' => 5, 'region' => 'content'));
+ $this->assertNull($display->getComponent('display_extra_field_hidden'));
+
+ // Check that setting explicit options overrides the defaults.
+ $display->removeComponent('display_extra_field');
+ $display->setComponent('display_extra_field_hidden', array('weight' => 10));
+ $this->assertNull($display->getComponent('display_extra_field'));
+ $this->assertEqual($display->getComponent('display_extra_field_hidden'), array('weight' => 10, 'settings' => array(), 'third_party_settings' => array()));
+ }
+
+ /**
+ * Tests the behavior of an extra field component with initial invalid values.
+ */
+ public function testExtraFieldComponentInitialInvalidConfig() {
+ entity_test_create_bundle('bundle_with_extra_fields');
+ $display = EntityViewDisplay::create(array(
+ 'targetEntityType' => 'entity_test',
+ 'bundle' => 'bundle_with_extra_fields',
+ 'mode' => 'default',
+ // Add the extra field to the initial config, without a 'type'.
+ 'content' => [
+ 'display_extra_field' => [
+ 'weight' => 5,
+ ],
+ ],
+ ));
+
+ // Check that the default visibility taken into account for extra fields
+ // unknown in the display that were included in the initial config.
+ $this->assertEqual($display->getComponent('display_extra_field'), array('weight' => 5, 'region' => 'content'));
$this->assertNull($display->getComponent('display_extra_field_hidden'));
// Check that setting explicit options overrides the defaults.
@@ -258,6 +289,7 @@ public function testBaseFieldComponent() {
'settings' => $formatter_settings,
'third_party_settings' => array(),
'weight' => 10,
+ 'region' => 'content',
),
'test_display_non_configurable' => array(
'label' => 'above',
@@ -265,6 +297,7 @@ public function testBaseFieldComponent() {
'settings' => $formatter_settings,
'third_party_settings' => array(),
'weight' => 11,
+ 'region' => 'content',
),
);
foreach ($expected as $field_name => $options) {
diff --git a/core/modules/field_ui/tests/src/Kernel/EntityFormDisplayTest.php b/core/modules/field_ui/tests/src/Kernel/EntityFormDisplayTest.php
index e38db7c..6f770d6 100644
--- a/core/modules/field_ui/tests/src/Kernel/EntityFormDisplayTest.php
+++ b/core/modules/field_ui/tests/src/Kernel/EntityFormDisplayTest.php
@@ -43,7 +43,7 @@ public function testEntityGetFromDisplay() {
$form_display = entity_get_form_display('entity_test', 'entity_test', 'default');
$this->assertFalse($form_display->isNew());
$this->assertEqual($form_display->id(), 'entity_test.entity_test.default');
- $this->assertEqual($form_display->getComponent('component_1'), array('weight' => 10, 'settings' => array(), 'third_party_settings' => array()));
+ $this->assertEqual($form_display->getComponent('component_1'), array('weight' => 10, 'settings' => array(), 'third_party_settings' => array(), 'region' => 'content'));
}
/**
@@ -134,12 +134,14 @@ public function testBaseFieldComponent() {
'settings' => $formatter_settings,
'third_party_settings' => array(),
'weight' => 10,
+ 'region' => 'content',
),
'test_display_non_configurable' => array(
'type' => 'text_textfield',
'settings' => $formatter_settings,
'third_party_settings' => array(),
'weight' => 11,
+ 'region' => 'content',
),
);
foreach ($expected as $field_name => $options) {
diff --git a/core/modules/file/src/Tests/FileFieldDisplayTest.php b/core/modules/file/src/Tests/FileFieldDisplayTest.php
index 142751a..b25b514 100644
--- a/core/modules/file/src/Tests/FileFieldDisplayTest.php
+++ b/core/modules/file/src/Tests/FileFieldDisplayTest.php
@@ -36,9 +36,17 @@ function testNodeDisplay() {
// case.
$file_formatters = array('file_table', 'file_url_plain', 'hidden', 'file_default');
foreach ($file_formatters as $formatter) {
- $edit = array(
- "fields[$field_name][type]" => $formatter,
- );
+ if ($formatter === 'hidden') {
+ $edit = [
+ "fields[$field_name][region]" => 'hidden',
+ ];
+ }
+ else {
+ $edit = [
+ "fields[$field_name][type]" => $formatter,
+ "fields[$field_name][region]" => 'content',
+ ];
+ }
$this->drupalPostForm("admin/structure/types/manage/$type_name/display", $edit, t('Save'));
$this->drupalGet('node/' . $node->id());
$this->assertNoText($field_name, format_string('Field label is hidden when no file attached for formatter %formatter', array('%formatter' => $formatter)));
diff --git a/core/modules/file/src/Tests/FileFieldRSSContentTest.php b/core/modules/file/src/Tests/FileFieldRSSContentTest.php
index 0422514..4e18e71 100644
--- a/core/modules/file/src/Tests/FileFieldRSSContentTest.php
+++ b/core/modules/file/src/Tests/FileFieldRSSContentTest.php
@@ -37,7 +37,10 @@ function testFileFieldRSSContent() {
// Change the format to 'RSS enclosure'.
$this->drupalGet("admin/structure/types/manage/$type_name/display/rss");
- $edit = array("fields[$field_name][type]" => 'file_rss_enclosure');
+ $edit = array(
+ "fields[$field_name][type]" => 'file_rss_enclosure',
+ "fields[$field_name][region]" => 'content',
+ );
$this->drupalPostForm(NULL, $edit, t('Save'));
// Create a new node with a file field set. Promote to frontpage
diff --git a/core/modules/forum/config/optional/core.entity_form_display.comment.comment_forum.default.yml b/core/modules/forum/config/optional/core.entity_form_display.comment.comment_forum.default.yml
index a09c30b..4738bbb 100644
--- a/core/modules/forum/config/optional/core.entity_form_display.comment.comment_forum.default.yml
+++ b/core/modules/forum/config/optional/core.entity_form_display.comment.comment_forum.default.yml
@@ -13,9 +13,11 @@ mode: default
content:
author:
weight: -2
+ region: content
comment_body:
type: text_textarea
weight: 11
+ region: content
settings:
rows: 5
placeholder: ''
@@ -23,6 +25,7 @@ content:
subject:
type: string_textfield
weight: 10
+ region: content
settings:
size: 60
placeholder: ''
diff --git a/core/modules/forum/config/optional/core.entity_form_display.node.forum.default.yml b/core/modules/forum/config/optional/core.entity_form_display.node.forum.default.yml
index c66ba23..6773d32 100644
--- a/core/modules/forum/config/optional/core.entity_form_display.node.forum.default.yml
+++ b/core/modules/forum/config/optional/core.entity_form_display.node.forum.default.yml
@@ -17,6 +17,7 @@ content:
body:
type: text_textarea_with_summary
weight: 27
+ region: content
settings:
rows: 9
summary_rows: 3
@@ -25,11 +26,13 @@ content:
comment_forum:
type: comment_default
weight: 20
+ region: content
settings: { }
third_party_settings: { }
created:
type: datetime_timestamp
weight: 10
+ region: content
settings: { }
third_party_settings: { }
promote:
@@ -37,21 +40,25 @@ content:
settings:
display_label: true
weight: 15
+ region: content
third_party_settings: { }
sticky:
type: boolean_checkbox
settings:
display_label: true
weight: 16
+ region: content
third_party_settings: { }
taxonomy_forums:
type: options_select
weight: 26
+ region: content
settings: { }
third_party_settings: { }
title:
type: string_textfield
weight: -5
+ region: content
settings:
size: 60
placeholder: ''
@@ -59,6 +66,7 @@ content:
uid:
type: entity_reference_autocomplete
weight: 5
+ region: content
settings:
match_operator: CONTAINS
size: 60
diff --git a/core/modules/forum/config/optional/core.entity_form_display.taxonomy_term.forums.default.yml b/core/modules/forum/config/optional/core.entity_form_display.taxonomy_term.forums.default.yml
index b18c869..50df98a 100644
--- a/core/modules/forum/config/optional/core.entity_form_display.taxonomy_term.forums.default.yml
+++ b/core/modules/forum/config/optional/core.entity_form_display.taxonomy_term.forums.default.yml
@@ -14,11 +14,13 @@ content:
description:
type: text_textfield
weight: 0
+ region: content
settings: { }
third_party_settings: { }
name:
type: string_textfield
weight: -5
+ region: content
settings:
size: 60
placeholder: ''
diff --git a/core/modules/forum/config/optional/core.entity_view_display.comment.comment_forum.default.yml b/core/modules/forum/config/optional/core.entity_view_display.comment.comment_forum.default.yml
index f4f0112..befeba8 100644
--- a/core/modules/forum/config/optional/core.entity_view_display.comment.comment_forum.default.yml
+++ b/core/modules/forum/config/optional/core.entity_view_display.comment.comment_forum.default.yml
@@ -15,8 +15,10 @@ content:
label: hidden
type: text_default
weight: 0
+ region: content
settings: { }
third_party_settings: { }
links:
weight: 100
+ region: content
hidden: { }
diff --git a/core/modules/forum/config/optional/core.entity_view_display.node.forum.default.yml b/core/modules/forum/config/optional/core.entity_view_display.node.forum.default.yml
index b157c83..f3e8c5c 100644
--- a/core/modules/forum/config/optional/core.entity_view_display.node.forum.default.yml
+++ b/core/modules/forum/config/optional/core.entity_view_display.node.forum.default.yml
@@ -20,21 +20,25 @@ content:
label: hidden
type: text_default
weight: 0
+ region: content
settings: { }
third_party_settings: { }
comment_forum:
label: hidden
type: comment_default
weight: 20
+ region: content
settings:
view_mode: default
pager_id: 0
third_party_settings: { }
links:
weight: 100
+ region: content
taxonomy_forums:
type: entity_reference_label
weight: -1
+ region: content
label: above
settings:
link: true
diff --git a/core/modules/forum/config/optional/core.entity_view_display.node.forum.teaser.yml b/core/modules/forum/config/optional/core.entity_view_display.node.forum.teaser.yml
index 4405e71..7b174f4 100644
--- a/core/modules/forum/config/optional/core.entity_view_display.node.forum.teaser.yml
+++ b/core/modules/forum/config/optional/core.entity_view_display.node.forum.teaser.yml
@@ -19,14 +19,17 @@ content:
label: hidden
type: text_summary_or_trimmed
weight: 100
+ region: content
settings:
trim_length: 600
third_party_settings: { }
links:
weight: 101
+ region: content
taxonomy_forums:
type: entity_reference_label
weight: 10
+ region: content
label: above
settings:
link: true
diff --git a/core/modules/forum/config/optional/core.entity_view_display.taxonomy_term.forums.default.yml b/core/modules/forum/config/optional/core.entity_view_display.taxonomy_term.forums.default.yml
index d1242d9..b326039 100644
--- a/core/modules/forum/config/optional/core.entity_view_display.taxonomy_term.forums.default.yml
+++ b/core/modules/forum/config/optional/core.entity_view_display.taxonomy_term.forums.default.yml
@@ -14,6 +14,7 @@ content:
description:
type: text_default
weight: 0
+ region: content
settings: { }
third_party_settings: { }
label: above
diff --git a/core/modules/layout_plugin/config/schema/layout_plugin.schema.yml b/core/modules/layout_plugin/config/schema/layout_plugin.schema.yml
new file mode 100644
index 0000000..f850db3
--- /dev/null
+++ b/core/modules/layout_plugin/config/schema/layout_plugin.schema.yml
@@ -0,0 +1,6 @@
+layout_plugin.settings:
+ type: mapping
+ label: 'Layout settings'
+
+layout_plugin.settings.*:
+ type: layout_plugin.settings
diff --git a/core/modules/layout_plugin/layout_plugin.api.php b/core/modules/layout_plugin/layout_plugin.api.php
new file mode 100644
index 0000000..e77fd9f
--- /dev/null
+++ b/core/modules/layout_plugin/layout_plugin.api.php
@@ -0,0 +1,26 @@
+' . t('About') . '';
+ $output .= '
' . t('Layout Plugin allows modules or themes to register layouts, and for other modules to list the available layouts and render them.') . '
';
+ $output .= '
' . t('For more information, see the online documentation for the Layout Plugin module.', [':layout-plugin-documentation' => 'https://www.drupal.org/node/2619128']) . '
';
+ return $output;
+ }
+}
+
+/**
+ * Implements hook_theme().
+ */
+function layout_plugin_theme($existing, $type, $theme, $path) {
+ return \Drupal::service('plugin.manager.layout_plugin')->getThemeImplementations();
+}
diff --git a/core/modules/layout_plugin/layout_plugin.services.yml b/core/modules/layout_plugin/layout_plugin.services.yml
new file mode 100644
index 0000000..74225de
--- /dev/null
+++ b/core/modules/layout_plugin/layout_plugin.services.yml
@@ -0,0 +1,4 @@
+services:
+ plugin.manager.layout_plugin:
+ class: Drupal\layout_plugin\LayoutPluginManager
+ arguments: ['@container.namespaces', '@cache.discovery', '@module_handler', '@theme_handler']
diff --git a/core/modules/layout_plugin/src/Annotation/Layout.php b/core/modules/layout_plugin/src/Annotation/Layout.php
new file mode 100644
index 0000000..3a615da
--- /dev/null
+++ b/core/modules/layout_plugin/src/Annotation/Layout.php
@@ -0,0 +1,150 @@
+definition);
+ }
+
+}
diff --git a/core/modules/layout_plugin/src/DerivablePluginDefinitionInterface.php b/core/modules/layout_plugin/src/DerivablePluginDefinitionInterface.php
new file mode 100644
index 0000000..1818685
--- /dev/null
+++ b/core/modules/layout_plugin/src/DerivablePluginDefinitionInterface.php
@@ -0,0 +1,37 @@
+themeHandler = $theme_handler;
+
+ $this->setCacheBackend($cache_backend, 'layout');
+ $this->alterInfo('layout');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function providerExists($provider) {
+ return $this->moduleHandler->moduleExists($provider) || $this->themeHandler->themeExists($provider);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getDiscovery() {
+ if (!$this->discovery) {
+ $discovery = new AnnotatedClassDiscovery($this->subdir, $this->namespaces, $this->pluginDefinitionAnnotationName, $this->additionalAnnotationNamespaces);
+ $discovery = new YamlDiscoveryDecorator($discovery, 'layouts', $this->moduleHandler->getModuleDirectories() + $this->themeHandler->getThemeDirectories());
+ $discovery = new ObjectDefinitionDiscoveryDecorator($discovery, $this->pluginDefinitionAnnotationName);
+ $discovery = new ObjectDefinitionContainerDerivativeDiscoveryDecorator($discovery);
+ $this->discovery = $discovery;
+ }
+ return $this->discovery;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function processDefinition(&$definition, $plugin_id) {
+ parent::processDefinition($definition, $plugin_id);
+
+ if (!$definition instanceof LayoutDefinition) {
+ throw new InvalidPluginDefinitionException($plugin_id, sprintf('The "%s" layout definition must extend %s', $plugin_id, LayoutDefinition::class));
+ }
+
+ // Keep class definitions standard with no leading slash.
+ $definition->setClass(ltrim($definition->getClass(), '\\'));
+
+ // Add the module or theme path to the 'path'.
+ $provider = $definition->getProvider();
+ if ($this->moduleHandler->moduleExists($provider)) {
+ $base_path = $this->moduleHandler->getModule($provider)->getPath();
+ }
+ elseif ($this->themeHandler->themeExists($provider)) {
+ $base_path = $this->themeHandler->getTheme($provider)->getPath();
+ }
+ else {
+ $base_path = '';
+ }
+
+ $path = $definition->getPath();
+ $path = !empty($path) ? $base_path . '/' . $path : $base_path;
+ $definition->setPath($path);
+
+ // Add the base path to the icon path.
+ if ($icon_path = $definition->getIconPath()) {
+ $definition->setIconPath($path . '/' . $icon_path);
+ }
+
+ // Add a dependency on the provider of the library.
+ if ($library = $definition->getLibrary()) {
+ $config_dependencies = $definition->getConfigDependencies();
+ list($library_provider) = explode('/', $library, 2);
+ if ($this->moduleHandler->moduleExists($library_provider)) {
+ $config_dependencies['module'][] = $library_provider;
+ }
+ elseif ($this->themeHandler->themeExists($library_provider)) {
+ $config_dependencies['theme'][] = $library_provider;
+ }
+ $definition->setConfigDependencies($config_dependencies);
+ }
+
+ // If 'template' is set, then we'll derive 'template_path' and 'theme_hook'.
+ $template = $definition->getTemplate();
+ if (!empty($template)) {
+ $template_parts = explode('/', $template);
+
+ $template = array_pop($template_parts);
+ $template_path = $path;
+ if (count($template_parts) > 0) {
+ $template_path .= '/' . implode('/', $template_parts);
+ }
+ $definition->setTemplate($template);
+ $definition->setThemeHook(strtr($template, '-', '_'));
+ $definition->setTemplatePath($template_path);
+ }
+
+ if (!$definition->getDefaultRegion()) {
+ $definition->setDefaultRegion(key($definition->getRegions()));
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getThemeImplementations() {
+ $hooks = [];
+ /** @var \Drupal\layout_plugin\Plugin\Layout\LayoutDefinition[] $definitions */
+ $definitions = $this->getDefinitions();
+ foreach ($definitions as $definition) {
+ if ($template = $definition->getTemplate()) {
+ $hooks[$definition->getThemeHook()] = [
+ 'variables' => [
+ 'content' => [],
+ 'settings' => [],
+ 'layout' => [],
+ ],
+ 'template' => $template,
+ 'path' => $definition->getTemplatePath(),
+ ];
+ }
+ }
+ return $hooks;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCategories() {
+ // Fetch all categories from definitions and remove duplicates.
+ $categories = array_unique(array_values(array_map(function (LayoutDefinition $definition) {
+ return $definition->getCategory();
+ }, $this->getDefinitions())));
+ natcasesort($categories);
+ return $categories;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return \Drupal\layout_plugin\Plugin\Layout\LayoutDefinition[]
+ */
+ public function getSortedDefinitions(array $definitions = NULL, $label_key = 'label') {
+ // Sort the plugins first by category, then by label.
+ $definitions = isset($definitions) ? $definitions : $this->getDefinitions();
+ // Suppress errors because PHPUnit will indirectly modify the contents,
+ // triggering https://bugs.php.net/bug.php?id=50688.
+ @uasort($definitions, function (LayoutDefinition $a, LayoutDefinition $b) {
+ if ($a->getCategory() != $b->getCategory()) {
+ return strnatcasecmp($a->getCategory(), $b->getCategory());
+ }
+ return strnatcasecmp($a->getLabel(), $b->getLabel());
+ });
+ return $definitions;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return \Drupal\layout_plugin\Plugin\Layout\LayoutDefinition[][]
+ */
+ public function getGroupedDefinitions(array $definitions = NULL, $label_key = 'label') {
+ $definitions = $this->getSortedDefinitions(isset($definitions) ? $definitions : $this->getDefinitions(), $label_key);
+ $grouped_definitions = [];
+ foreach ($definitions as $id => $definition) {
+ $grouped_definitions[(string) $definition->getCategory()][$id] = $definition;
+ }
+ return $grouped_definitions;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getLayoutOptions() {
+ $layout_options = [];
+ foreach ($this->getGroupedDefinitions() as $category => $layout_definitions) {
+ foreach ($layout_definitions as $name => $layout_definition) {
+ $layout_options[$category][$name] = $layout_definition->getLabel();
+ }
+ }
+ return $layout_options;
+ }
+
+}
diff --git a/core/modules/layout_plugin/src/LayoutPluginManagerInterface.php b/core/modules/layout_plugin/src/LayoutPluginManagerInterface.php
new file mode 100644
index 0000000..3f0a084
--- /dev/null
+++ b/core/modules/layout_plugin/src/LayoutPluginManagerInterface.php
@@ -0,0 +1,65 @@
+getDeriver()) {
+ if (!class_exists($class)) {
+ throw new InvalidDeriverException(sprintf('Plugin (%s) deriver "%s" does not exist.', $base_definition['id'], $class));
+ }
+ if (!is_subclass_of($class, '\Drupal\Component\Plugin\Derivative\DeriverInterface')) {
+ throw new InvalidDeriverException(sprintf('Plugin (%s) deriver "%s" must implement \Drupal\Component\Plugin\Derivative\DeriverInterface.', $base_definition['id'], $class));
+ }
+ }
+ return $class;
+ }
+
+}
diff --git a/core/modules/layout_plugin/src/ObjectDefinitionDiscoveryDecorator.php b/core/modules/layout_plugin/src/ObjectDefinitionDiscoveryDecorator.php
new file mode 100644
index 0000000..013f753
--- /dev/null
+++ b/core/modules/layout_plugin/src/ObjectDefinitionDiscoveryDecorator.php
@@ -0,0 +1,73 @@
+decorated = $decorated;
+ $this->pluginDefinitionAnnotationName = $plugin_definition_annotation_name;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDefinitions() {
+ $definitions = $this->decorated->getDefinitions();
+ foreach ($definitions as $id => $definition) {
+ if (is_array($definition)) {
+ $definitions[$id] = (new $this->pluginDefinitionAnnotationName($definition))->get();
+ }
+ }
+ return $definitions;
+ }
+
+ /**
+ * Passes through all unknown calls onto the decorated object.
+ *
+ * @param string $method
+ * The method to call on the decorated plugin discovery.
+ * @param array $args
+ * The arguments to send to the method.
+ *
+ * @return mixed
+ * The method result.
+ */
+ public function __call($method, $args) {
+ return call_user_func_array([$this->decorated, $method], $args);
+ }
+
+}
diff --git a/core/modules/layout_plugin/src/Plugin/Layout/LayoutBase.php b/core/modules/layout_plugin/src/Plugin/Layout/LayoutBase.php
new file mode 100644
index 0000000..964bce6
--- /dev/null
+++ b/core/modules/layout_plugin/src/Plugin/Layout/LayoutBase.php
@@ -0,0 +1,79 @@
+setConfiguration($configuration);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function build(array $regions) {
+ $build['#content'] = array_intersect_key($regions, $this->pluginDefinition->getRegions());
+ $build['#settings'] = $this->getConfiguration();
+ $build['#layout'] = $this->pluginDefinition;
+ $build['#theme'] = $this->pluginDefinition->getThemeHook();
+ if ($library = $this->pluginDefinition->getLibrary()) {
+ $build['#attached']['library'][] = $library;
+ }
+ return $build;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConfiguration() {
+ return $this->configuration;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setConfiguration(array $configuration) {
+ $this->configuration = NestedArray::mergeDeep($this->defaultConfiguration(), $configuration);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration() {
+ return [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function calculateDependencies() {
+ return [];
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @return \Drupal\layout_plugin\Plugin\Layout\LayoutDefinition
+ */
+ public function getPluginDefinition() {
+ return parent::getPluginDefinition();
+ }
+
+}
diff --git a/core/modules/layout_plugin/src/Plugin/Layout/LayoutDefault.php b/core/modules/layout_plugin/src/Plugin/Layout/LayoutDefault.php
new file mode 100644
index 0000000..462b68d
--- /dev/null
+++ b/core/modules/layout_plugin/src/Plugin/Layout/LayoutDefault.php
@@ -0,0 +1,10 @@
+ $value) {
+ $this->set($property, $value);
+ }
+ }
+
+ /**
+ * Gets any arbitrary property.
+ *
+ * @param string $property
+ * The property to retrieve.
+ *
+ * @return mixed
+ * The value for that property, or NULL if the property does not exist.
+ */
+ public function get($property) {
+ if (property_exists($this, $property)) {
+ $value = isset($this->{$property}) ? $this->{$property} : NULL;
+ }
+ else {
+ $value = isset($this->additional[$property]) ? $this->additional[$property] : NULL;
+ }
+ return $value;
+ }
+
+ /**
+ * Sets a value to an arbitrary property.
+ *
+ * @param string $property
+ * The property to use for the value.
+ * @param mixed $value
+ * The value to set.
+ *
+ * @return $this
+ */
+ public function set($property, $value) {
+ if (property_exists($this, $property)) {
+ $this->{$property} = $value;
+ }
+ else {
+ $this->additional[$property] = $value;
+ }
+ return $this;
+ }
+
+ /**
+ * Gets the unique identifier of the layout definition.
+ *
+ * @return string
+ * The unique identifier of the layout definition.
+ */
+ public function id() {
+ return $this->id;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getClass() {
+ return $this->class;
+ }
+
+ /**
+ * Gets the name of the original layout class.
+ *
+ * In case the class name was changed with setClass(), this will return
+ * the initial value.
+ *
+ * @return string
+ * The name of the original layout class.
+ */
+ public function getOriginalClass() {
+ return $this->originalClass ?: $this->class;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setClass($class) {
+ if (!$this->originalClass && $this->class) {
+ // If the original class is currently not set, set it to the current
+ // class, assume that is the original class name.
+ $this->originalClass = $this->class;
+ }
+ $this->class = $class;
+ return $this;
+ }
+
+ /**
+ * Gets the human-readable name of the layout definition.
+ *
+ * @return string|\Drupal\Core\StringTranslation\TranslatableMarkup
+ * The human-readable name of the layout definition.
+ */
+ public function getLabel() {
+ return $this->label;
+ }
+
+ /**
+ * Sets the human-readable name of the layout definition.
+ *
+ * @param string|\Drupal\Core\StringTranslation\TranslatableMarkup $label
+ * The human-readable name of the layout definition.
+ *
+ * @return $this
+ */
+ public function setLabel($label) {
+ $this->label = $label;
+ return $this;
+ }
+
+ /**
+ * Gets the description of the layout definition.
+ *
+ * @return string|\Drupal\Core\StringTranslation\TranslatableMarkup
+ * The description of the layout definition.
+ */
+ public function getDescription() {
+ return $this->description;
+ }
+
+ /**
+ * Sets the description of the layout definition.
+ *
+ * @param string|\Drupal\Core\StringTranslation\TranslatableMarkup $description
+ * The description of the layout definition.
+ *
+ * @return $this
+ */
+ public function setDescription($description) {
+ $this->description = $description;
+ return $this;
+ }
+
+ /**
+ * Gets the human-readable category of the layout definition.
+ *
+ * @return string|\Drupal\Core\StringTranslation\TranslatableMarkup
+ * The human-readable category of the layout definition.
+ */
+ public function getCategory() {
+ return $this->category;
+ }
+
+ /**
+ * Sets the human-readable category of the layout definition.
+ *
+ * @param string|\Drupal\Core\StringTranslation\TranslatableMarkup $category
+ * The human-readable category of the layout definition.
+ *
+ * @return $this
+ */
+ public function setCategory($category) {
+ $this->category = $category;
+ return $this;
+ }
+
+ /**
+ * Gets the template name.
+ *
+ * @return string|null
+ * The template name, if it exists.
+ */
+ public function getTemplate() {
+ return $this->template;
+ }
+
+ /**
+ * Sets the template name.
+ *
+ * @param string|null $template
+ * The template name.
+ *
+ * @return $this
+ */
+ public function setTemplate($template) {
+ $this->template = $template;
+ return $this;
+ }
+
+ /**
+ * Gets the template path.
+ *
+ * @return string
+ * The template path.
+ */
+ public function getTemplatePath() {
+ return $this->templatePath;
+ }
+
+ /**
+ * Sets the template path.
+ *
+ * @param string $template_path
+ * The template path.
+ *
+ * @return $this
+ */
+ public function setTemplatePath($template_path) {
+ $this->templatePath = $template_path;
+ return $this;
+ }
+
+ /**
+ * Gets the theme hook.
+ *
+ * @return string|null
+ * The theme hook, if it exists.
+ */
+ public function getThemeHook() {
+ return $this->theme_hook;
+ }
+
+ /**
+ * Sets the theme hook.
+ *
+ * @param string $theme_hook
+ * The theme hook.
+ *
+ * @return $this
+ */
+ public function setThemeHook($theme_hook) {
+ $this->theme_hook = $theme_hook;
+ return $this;
+ }
+
+ /**
+ * Gets the base path for this layout definition.
+ *
+ * @return string
+ * The base path.
+ */
+ public function getPath() {
+ return $this->path;
+ }
+
+ /**
+ * Sets the base path for this layout definition.
+ *
+ * @param string $path
+ * The base path.
+ *
+ * @return $this
+ */
+ public function setPath($path) {
+ $this->path = $path;
+ return $this;
+ }
+
+ /**
+ * Gets the asset library for this layout definition.
+ *
+ * @return string|null
+ * The asset library, if it exists.
+ */
+ public function getLibrary() {
+ return $this->library;
+ }
+
+ /**
+ * Sets the asset library for this layout definition.
+ *
+ * @param string|null $library
+ * The asset library.
+ *
+ * @return $this
+ */
+ public function setLibrary($library) {
+ $this->library = $library;
+ return $this;
+ }
+
+ /**
+ * Gets the icon path for this layout definition.
+ *
+ * @return string|null
+ * The icon path, if it exists.
+ */
+ public function getIconPath() {
+ return $this->icon;
+ }
+
+ /**
+ * Sets the icon path for this layout definition.
+ *
+ * @param string|null $icon
+ * The icon path.
+ *
+ * @return $this
+ */
+ public function setIconPath($icon) {
+ $this->icon = $icon;
+ return $this;
+ }
+
+ /**
+ * Gets the regions for this layout definition.
+ *
+ * @return array[]
+ * The layout regions. The keys of the array are the machine names of the
+ * regions, and the values are an associative array with the following
+ * keys:
+ * - label: (string) The human-readable name of the region.
+ * Any remaining keys may have special meaning for the given layout plugin,
+ * but are undefined here.
+ */
+ public function getRegions() {
+ return $this->regions;
+ }
+
+ /**
+ * Sets the regions for this layout definition.
+ *
+ * @param array[] $regions
+ * An array of regions, see ::getRegions() for the format.
+ *
+ * @return $this
+ */
+ public function setRegions(array $regions) {
+ $this->regions = $regions;
+ return $this;
+ }
+
+ /**
+ * Gets the machine-readable region names.
+ *
+ * @return string[]
+ * An array of machine-readable region names.
+ */
+ public function getRegionNames() {
+ return array_keys($this->getRegions());
+ }
+
+ /**
+ * Gets the human-readable region labels.
+ *
+ * @return string[]
+ * An array of human-readable region labels.
+ */
+ public function getRegionLabels() {
+ $regions = $this->getRegions();
+ return array_combine(array_keys($regions), array_column($regions, 'label'));
+ }
+
+ /**
+ * Gets the default region.
+ *
+ * @return string
+ * The machine-readable name of the default region.
+ */
+ public function getDefaultRegion() {
+ return $this->default_region;
+ }
+
+ /**
+ * Sets the default region.
+ *
+ * @param string $default_region
+ * The machine-readable name of the default region.
+ *
+ * @return $this
+ */
+ public function setDefaultRegion($default_region) {
+ $this->default_region = $default_region;
+ return $this;
+ }
+
+ /**
+ * Gets the name of the provider of this layout definition.
+ *
+ * @return string
+ * The name of the provider of this layout definition.
+ */
+ public function getProvider() {
+ return $this->provider;
+ }
+
+ /**
+ * Gets the config dependencies of this layout definition.
+ *
+ * @return array
+ * An array of config dependencies.
+ *
+ * @see \Drupal\Core\Plugin\PluginDependencyTrait::calculatePluginDependencies()
+ */
+ public function getConfigDependencies() {
+ return $this->config_dependencies;
+ }
+
+ /**
+ * Sets the config dependencies of this layout definition.
+ *
+ * @param array $config_dependencies
+ * An array of config dependencies.
+ *
+ * @return $this
+ */
+ public function setConfigDependencies(array $config_dependencies) {
+ $this->config_dependencies = $config_dependencies;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDeriver() {
+ return $this->deriver;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDeriver($deriver) {
+ $this->deriver = $deriver;
+ return $this;
+ }
+
+}
diff --git a/core/modules/layout_plugin/src/Plugin/Layout/LayoutInterface.php b/core/modules/layout_plugin/src/Plugin/Layout/LayoutInterface.php
new file mode 100644
index 0000000..ccfd68e
--- /dev/null
+++ b/core/modules/layout_plugin/src/Plugin/Layout/LayoutInterface.php
@@ -0,0 +1,33 @@
+ 'Default',
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+ $form['setting_1'] = [
+ '#type' => 'textfield',
+ '#title' => 'Blah',
+ '#default_value' => $this->configuration['setting_1'],
+ ];
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+ $this->configuration['setting_1'] = $form_state->getValue('setting_1');
+ }
+
+}
diff --git a/core/modules/layout_plugin/tests/modules/layout_test/templates/layout-test-1col.html.twig b/core/modules/layout_plugin/tests/modules/layout_test/templates/layout-test-1col.html.twig
new file mode 100644
index 0000000..e7a7eb5
--- /dev/null
+++ b/core/modules/layout_plugin/tests/modules/layout_test/templates/layout-test-1col.html.twig
@@ -0,0 +1,14 @@
+{#
+/**
+ * @file
+ * Template for an example 1 column layout.
+ */
+#}
+
+
+ {{ content.top }}
+
+
+ {{ content.bottom }}
+
+
diff --git a/core/modules/layout_plugin/tests/modules/layout_test/templates/layout-test-2col.html.twig b/core/modules/layout_plugin/tests/modules/layout_test/templates/layout-test-2col.html.twig
new file mode 100644
index 0000000..11433ee
--- /dev/null
+++ b/core/modules/layout_plugin/tests/modules/layout_test/templates/layout-test-2col.html.twig
@@ -0,0 +1,14 @@
+{#
+/**
+ * @file
+ * Template for an example 2 column layout.
+ */
+#}
+
+
+ {{ content.left }}
+
+
+ {{ content.right }}
+
+
diff --git a/core/modules/layout_plugin/tests/modules/layout_test/templates/layout-test-plugin.html.twig b/core/modules/layout_plugin/tests/modules/layout_test/templates/layout-test-plugin.html.twig
new file mode 100644
index 0000000..e49942c
--- /dev/null
+++ b/core/modules/layout_plugin/tests/modules/layout_test/templates/layout-test-plugin.html.twig
@@ -0,0 +1,15 @@
+{#
+/**
+ * @file
+ * Template for layout_test_plugin layout.
+ */
+#}
+
+
+ Blah:
+ {{ settings.setting_1 }}
+
+
+ {{ content.main }}
+
+
diff --git a/core/modules/layout_plugin/tests/src/Kernel/LayoutTest.php b/core/modules/layout_plugin/tests/src/Kernel/LayoutTest.php
new file mode 100644
index 0000000..4c5a2a0
--- /dev/null
+++ b/core/modules/layout_plugin/tests/src/Kernel/LayoutTest.php
@@ -0,0 +1,127 @@
+layoutPluginManager = $this->container->get('plugin.manager.layout_plugin');
+ }
+
+ /**
+ * Test rendering a layout.
+ *
+ * @dataProvider renderLayoutData
+ */
+ public function testRenderLayout($layout_id, $config, $regions, $html) {
+ /** @var \Drupal\layout_plugin\Plugin\Layout\LayoutInterface $layout */
+ $layout = $this->layoutPluginManager->createInstance($layout_id, $config);
+ $built = $layout->build($regions);
+ $this->render($built);
+ $this->assertRaw($html);
+ }
+
+ /**
+ * Data provider for testRenderLayout().
+ */
+ public function renderLayoutData() {
+ $data['layout_test_1col'] = [
+ 'layout_test_1col',
+ [],
+ [
+ 'top' => [
+ '#markup' => 'This is the top',
+ ],
+ 'bottom' => [
+ '#markup' => 'This is the bottom',
+ ],
+ ],
+ ];
+
+ $data['layout_test_2col'] = [
+ 'layout_test_2col',
+ [],
+ [
+ 'left' => [
+ '#markup' => 'This is the left',
+ ],
+ 'right' => [
+ '#markup' => 'This is the right',
+ ],
+ ],
+ ];
+
+ $data['layout_test_plugin'] = [
+ 'layout_test_plugin',
+ [
+ 'setting_1' => 'Config value',
+ ],
+ [
+ 'main' => [
+ '#markup' => 'Main region',
+ ],
+ ],
+ ];
+
+ $data['layout_test_1col'][] = <<<'EOD'
+
+
+ This is the top
+
+
+ This is the bottom
+
+
+EOD;
+
+ $data['layout_test_2col'][] = <<<'EOD'
+
+
+ This is the left
+
+
+ This is the right
+
+
+EOD;
+
+ $data['layout_test_plugin'][] = <<<'EOD'
+
+
+ Blah:
+ Config value
+
+
+ Main region
+
+
+EOD;
+
+ return $data;
+ }
+
+}
diff --git a/core/modules/layout_plugin/tests/src/Unit/LayoutPluginManagerTest.php b/core/modules/layout_plugin/tests/src/Unit/LayoutPluginManagerTest.php
new file mode 100644
index 0000000..ee4978f
--- /dev/null
+++ b/core/modules/layout_plugin/tests/src/Unit/LayoutPluginManagerTest.php
@@ -0,0 +1,392 @@
+setUpFilesystem();
+
+ $container = new ContainerBuilder();
+ $container->set('string_translation', $this->getStringTranslationStub());
+ \Drupal::setContainer($container);
+
+ $this->moduleHandler = $this->prophesize(ModuleHandlerInterface::class);
+
+ $this->moduleHandler->moduleExists('module_a')->willReturn(TRUE);
+ $this->moduleHandler->moduleExists('theme_a')->willReturn(FALSE);
+ $this->moduleHandler->moduleExists('core')->willReturn(FALSE);
+ $this->moduleHandler->moduleExists('invalid_provider')->willReturn(FALSE);
+
+ $module_a = new Extension('/', 'module', vfsStream::url('root/modules/module_a/module_a.layouts.yml'));
+ $this->moduleHandler->getModule('module_a')->willReturn($module_a);
+ $this->moduleHandler->getModuleDirectories()->willReturn(['module_a' => vfsStream::url('root/modules/module_a')]);
+ $this->moduleHandler->alter('layout', Argument::type('array'))->shouldBeCalled();
+
+ $this->themeHandler = $this->prophesize(ThemeHandlerInterface::class);
+
+ $this->themeHandler->themeExists('theme_a')->willReturn(TRUE);
+ $this->themeHandler->themeExists('core')->willReturn(FALSE);
+ $this->themeHandler->themeExists('invalid_provider')->willReturn(FALSE);
+
+ $theme_a = new Extension('/', 'theme', vfsStream::url('root/themes/theme_a/theme_a.layouts.yml'));
+ $this->themeHandler->getTheme('theme_a')->willReturn($theme_a);
+ $this->themeHandler->getThemeDirectories()->willReturn(['theme_a' => vfsStream::url('root/themes/theme_a')]);
+
+ $this->cacheBackend = $this->prophesize(CacheBackendInterface::class);
+
+ $namespaces = new \ArrayObject(['Drupal\Core' => vfsStream::url('root/core/lib/Drupal/Core')]);
+ $this->layoutPluginManager = new LayoutPluginManager($namespaces, $this->cacheBackend->reveal(), $this->moduleHandler->reveal(), $this->themeHandler->reveal(), $this->getStringTranslationStub());
+ }
+
+ /**
+ * @covers ::getDefinitions
+ * @covers ::providerExists
+ */
+ public function testGetDefinitions() {
+ $expected = [
+ 'module_a_provided_layout',
+ 'theme_a_provided_layout',
+ 'plugin_provided_layout',
+ ];
+
+ $layout_definitions = $this->layoutPluginManager->getDefinitions();
+ $this->assertEquals($expected, array_keys($layout_definitions));
+ $this->assertContainsOnlyInstancesOf(LayoutDefinition::class, $layout_definitions);
+ }
+
+ /**
+ * @covers ::getDefinition
+ * @covers ::processDefinition
+ */
+ public function testGetDefinition() {
+ $theme_a_path = vfsStream::url('root/themes/theme_a');
+ $layout_definition = $this->layoutPluginManager->getDefinition('theme_a_provided_layout');
+ $this->assertSame('theme_a_provided_layout', $layout_definition->id());
+ $this->assertSame('2 column layout', $layout_definition->getLabel());
+ $this->assertSame('Columns: 2', $layout_definition->getCategory());
+ $this->assertSame('twocol', $layout_definition->getTemplate());
+ $this->assertSame("$theme_a_path/templates", $layout_definition->getPath());
+ $this->assertSame('theme_a/twocol', $layout_definition->getLibrary());
+ $this->assertSame('twocol', $layout_definition->getThemeHook());
+ $this->assertSame("$theme_a_path/templates", $layout_definition->getTemplatePath());
+ $this->assertSame('theme_a', $layout_definition->getProvider());
+ $this->assertSame('right', $layout_definition->getDefaultRegion());
+ $this->assertSame(LayoutDefault::class, $layout_definition->getClass());
+ $expected_regions = [
+ 'left' => [
+ 'label' => 'Left region',
+ ],
+ 'right' => [
+ 'label' => 'Right region',
+ ],
+ ];
+ $this->assertSame($expected_regions, $layout_definition->getRegions());
+
+ $module_a_path = vfsStream::url('root/modules/module_a');
+ $layout_definition = $this->layoutPluginManager->getDefinition('module_a_provided_layout');
+ $this->assertSame('module_a_provided_layout', $layout_definition->id());
+ $this->assertSame('1 column layout', $layout_definition->getLabel());
+ $this->assertSame('Columns: 1', $layout_definition->getCategory());
+ $this->assertSame(NULL, $layout_definition->getTemplate());
+ $this->assertSame("$module_a_path/layouts", $layout_definition->getPath());
+ $this->assertSame('module_a/onecol', $layout_definition->getLibrary());
+ $this->assertSame('onecol', $layout_definition->getThemeHook());
+ $this->assertSame(NULL, $layout_definition->getTemplatePath());
+ $this->assertSame('module_a', $layout_definition->getProvider());
+ $this->assertSame('top', $layout_definition->getDefaultRegion());
+ $this->assertSame(LayoutDefault::class, $layout_definition->getClass());
+ $expected_regions = [
+ 'top' => [
+ 'label' => 'Top region',
+ ],
+ 'bottom' => [
+ 'label' => 'Bottom region',
+ ],
+ ];
+ $this->assertSame($expected_regions, $layout_definition->getRegions());
+
+ $core_path = '/core/lib/Drupal/Core';
+ $layout_definition = $this->layoutPluginManager->getDefinition('plugin_provided_layout');
+ $this->assertSame('plugin_provided_layout', $layout_definition->id());
+ $this->assertEquals('Layout plugin', $layout_definition->getLabel());
+ $this->assertEquals('Columns: 1', $layout_definition->getCategory());
+ $this->assertSame('plugin-provided-layout', $layout_definition->getTemplate());
+ $this->assertSame($core_path, $layout_definition->getPath());
+ $this->assertSame(NULL, $layout_definition->getLibrary());
+ $this->assertSame('plugin_provided_layout', $layout_definition->getThemeHook());
+ $this->assertSame("$core_path/templates", $layout_definition->getTemplatePath());
+ $this->assertSame('core', $layout_definition->getProvider());
+ $this->assertSame('main', $layout_definition->getDefaultRegion());
+ $this->assertSame('Drupal\Core\Plugin\Layout\TestLayout', $layout_definition->getClass());
+ $expected_regions = [
+ 'main' => [
+ 'label' => 'Main Region',
+ ],
+ ];
+ $this->assertEquals($expected_regions, $layout_definition->getRegions());
+ }
+
+ /**
+ * @covers ::processDefinition
+ */
+ public function testProcessDefinition() {
+ $this->moduleHandler->alter('layout', Argument::type('array'))->shouldNotBeCalled();
+ $this->setExpectedException(InvalidPluginDefinitionException::class, 'The "module_a_derived_layout:array_based" layout definition must extend ' . LayoutDefinition::class);
+ $module_a_provided_layout = <<<'EOS'
+module_a_derived_layout:
+ deriver: \Drupal\Tests\layout_plugin\Unit\LayoutDeriver
+ array_based: true
+EOS;
+ vfsStream::create([
+ 'modules' => [
+ 'module_a' => [
+ 'module_a.layouts.yml' => $module_a_provided_layout,
+ ],
+ ],
+ ]);
+ $this->layoutPluginManager->getDefinitions();
+ }
+
+ /**
+ * @covers ::getThemeImplementations
+ */
+ public function testGetThemeImplementations() {
+ $core_path = '/core/lib/Drupal/Core';
+ $theme_a_path = vfsStream::url('root/themes/theme_a');
+ $expected = [
+ 'twocol' => [
+ 'variables' => [
+ 'content' => [],
+ 'settings' => [],
+ 'layout' => [],
+ ],
+ 'template' => 'twocol',
+ 'path' => "$theme_a_path/templates",
+ ],
+ 'plugin_provided_layout' => [
+ 'variables' => [
+ 'content' => [],
+ 'settings' => [],
+ 'layout' => [],
+ ],
+ 'template' => 'plugin-provided-layout',
+ 'path' => "$core_path/templates",
+ ],
+ ];
+ $theme_implementations = $this->layoutPluginManager->getThemeImplementations();
+ $this->assertEquals($expected, $theme_implementations);
+ }
+
+ /**
+ * @covers ::getCategories
+ */
+ public function testGetCategories() {
+ $expected = [
+ 'Columns: 1',
+ 'Columns: 2',
+ ];
+ $categories = $this->layoutPluginManager->getCategories();
+ $this->assertEquals($expected, $categories);
+ }
+
+ /**
+ * @covers ::getSortedDefinitions
+ */
+ public function testGetSortedDefinitions() {
+ $expected = [
+ 'module_a_provided_layout',
+ 'plugin_provided_layout',
+ 'theme_a_provided_layout',
+ ];
+
+ $layout_definitions = $this->layoutPluginManager->getSortedDefinitions();
+ $this->assertEquals($expected, array_keys($layout_definitions));
+ $this->assertContainsOnlyInstancesOf(LayoutDefinition::class, $layout_definitions);
+ }
+
+ /**
+ * @covers ::getGroupedDefinitions
+ */
+ public function testGetGroupedDefinitions() {
+ $category_expected = [
+ 'Columns: 1' => [
+ 'module_a_provided_layout',
+ 'plugin_provided_layout',
+ ],
+ 'Columns: 2' => [
+ 'theme_a_provided_layout',
+ ],
+ ];
+
+ $definitions = $this->layoutPluginManager->getGroupedDefinitions();
+ $this->assertEquals(array_keys($category_expected), array_keys($definitions));
+ foreach ($category_expected as $category => $expected) {
+ $this->assertArrayHasKey($category, $definitions);
+ $this->assertEquals($expected, array_keys($definitions[$category]));
+ $this->assertContainsOnlyInstancesOf(LayoutDefinition::class, $definitions[$category]);
+ }
+ }
+
+ /**
+ * Sets up the filesystem with YAML files and annotated plugins.
+ */
+ protected function setUpFilesystem() {
+ $module_a_provided_layout = <<<'EOS'
+module_a_provided_layout:
+ label: 1 column layout
+ category: 'Columns: 1'
+ theme_hook: onecol
+ path: layouts
+ library: module_a/onecol
+ regions:
+ top:
+ label: Top region
+ bottom:
+ label: Bottom region
+module_a_derived_layout:
+ deriver: \Drupal\Tests\layout_plugin\Unit\LayoutDeriver
+ invalid_provider: true
+EOS;
+ $theme_a_provided_layout = <<<'EOS'
+theme_a_provided_layout:
+ class: '\Drupal\layout_plugin\Plugin\Layout\LayoutDefault'
+ label: 2 column layout
+ category: 'Columns: 2'
+ template: twocol
+ path: templates
+ library: theme_a/twocol
+ default_region: right
+ regions:
+ left:
+ label: Left region
+ right:
+ label: Right region
+EOS;
+ $plugin_provided_layout = <<<'EOS'
+ [
+ 'module_a' => [
+ 'module_a.layouts.yml' => $module_a_provided_layout,
+ ],
+ ],
+ ]);
+ vfsStream::create([
+ 'themes' => [
+ 'theme_a' => [
+ 'theme_a.layouts.yml' => $theme_a_provided_layout,
+ ],
+ ],
+ ]);
+ vfsStream::create([
+ 'core' => [
+ 'lib' => [
+ 'Drupal' => [
+ 'Core' => [
+ 'Plugin' => [
+ 'Layout' => [
+ 'TestLayout.php' => $plugin_provided_layout,
+ ],
+ ],
+ ],
+ ],
+ ],
+ ],
+ ]);
+ }
+
+}
+/**
+ * Provides a dynamic layout deriver for the test.
+ */
+class LayoutDeriver extends DeriverBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDerivativeDefinitions($base_plugin_definition) {
+ if ($base_plugin_definition->get('array_based')) {
+ $this->derivatives['array_based'] = [];
+ }
+ if ($base_plugin_definition->get('invalid_provider')) {
+ $this->derivatives['invalid_provider'] = new LayoutDefinition([
+ 'id' => 'invalid_provider',
+ 'provider' => 'invalid_provider',
+ ]);
+ }
+ return $this->derivatives;
+ }
+
+}
diff --git a/core/modules/node/src/Tests/NodeTypeInitialLanguageTest.php b/core/modules/node/src/Tests/NodeTypeInitialLanguageTest.php
index 73bfdb3..fc911e3 100644
--- a/core/modules/node/src/Tests/NodeTypeInitialLanguageTest.php
+++ b/core/modules/node/src/Tests/NodeTypeInitialLanguageTest.php
@@ -75,7 +75,7 @@ function testNodeTypeInitialLanguageDefaults() {
$language_display = $this->xpath('//*[@id="langcode"]');
$this->assert(!empty($language_display), 'Language field is visible on manage display tab.');
// Tests if the language field is hidden by default.
- $this->assertOptionSelected('edit-fields-langcode-type', 'hidden', 'Language is hidden by default on manage display tab.');
+ $this->assertOptionSelected('edit-fields-langcode-region', 'hidden', 'Language is hidden by default on manage display tab.');
// Changes the initial language settings.
$edit = array(
@@ -109,6 +109,7 @@ function testLanguageFieldVisibility() {
// Configures Language field formatter and check if it is saved.
$edit = array(
'fields[langcode][type]' => 'language',
+ 'fields[langcode][region]' => 'content',
);
$this->drupalPostForm('admin/structure/types/manage/article/display', $edit, t('Save'));
$this->drupalGet('admin/structure/types/manage/article/display');
diff --git a/core/modules/options/src/Tests/OptionsFieldUITest.php b/core/modules/options/src/Tests/OptionsFieldUITest.php
index d2c11f0..ee6cf2e 100644
--- a/core/modules/options/src/Tests/OptionsFieldUITest.php
+++ b/core/modules/options/src/Tests/OptionsFieldUITest.php
@@ -335,6 +335,7 @@ function testNodeDisplay() {
foreach ($file_formatters as $formatter) {
$edit = array(
"fields[$this->fieldName][type]" => $formatter,
+ "fields[$this->fieldName][region]" => 'content',
);
$this->drupalPostForm('admin/structure/types/manage/' . $this->typeName . '/display', $edit, t('Save'));
$this->drupalGet('node/' . $node->id());
diff --git a/core/modules/options/tests/options_config_install_test/config/install/core.entity_form_display.node.options_install_test.default.yml b/core/modules/options/tests/options_config_install_test/config/install/core.entity_form_display.node.options_install_test.default.yml
index 19a2ef7..ff5f0ec 100644
--- a/core/modules/options/tests/options_config_install_test/config/install/core.entity_form_display.node.options_install_test.default.yml
+++ b/core/modules/options/tests/options_config_install_test/config/install/core.entity_form_display.node.options_install_test.default.yml
@@ -14,6 +14,7 @@ content:
title:
type: string_textfield
weight: -5
+ region: content
settings:
size: 60
placeholder: ''
@@ -21,6 +22,7 @@ content:
uid:
type: entity_reference_autocomplete
weight: 5
+ region: content
settings:
match_operator: CONTAINS
size: 60
@@ -29,6 +31,7 @@ content:
created:
type: datetime_timestamp
weight: 10
+ region: content
settings: { }
third_party_settings: { }
promote:
@@ -36,16 +39,19 @@ content:
settings:
display_label: true
weight: 15
+ region: content
third_party_settings: { }
sticky:
type: boolean_checkbox
settings:
display_label: true
weight: 16
+ region: content
third_party_settings: { }
body:
type: text_textarea_with_summary
weight: 26
+ region: content
settings:
rows: 9
summary_rows: 3
diff --git a/core/modules/options/tests/options_config_install_test/config/install/core.entity_view_display.node.options_install_test.default.yml b/core/modules/options/tests/options_config_install_test/config/install/core.entity_view_display.node.options_install_test.default.yml
index c107b10..aaea1cb 100644
--- a/core/modules/options/tests/options_config_install_test/config/install/core.entity_view_display.node.options_install_test.default.yml
+++ b/core/modules/options/tests/options_config_install_test/config/install/core.entity_view_display.node.options_install_test.default.yml
@@ -14,10 +14,12 @@ mode: default
content:
links:
weight: 100
+ region: content
body:
label: hidden
type: text_default
weight: 101
+ region: content
settings: { }
third_party_settings: { }
hidden:
diff --git a/core/modules/options/tests/options_config_install_test/config/install/core.entity_view_display.node.options_install_test.teaser.yml b/core/modules/options/tests/options_config_install_test/config/install/core.entity_view_display.node.options_install_test.teaser.yml
index 3b472a7..6e79af9 100644
--- a/core/modules/options/tests/options_config_install_test/config/install/core.entity_view_display.node.options_install_test.teaser.yml
+++ b/core/modules/options/tests/options_config_install_test/config/install/core.entity_view_display.node.options_install_test.teaser.yml
@@ -15,10 +15,12 @@ mode: teaser
content:
links:
weight: 100
+ region: content
body:
label: hidden
type: text_summary_or_trimmed
weight: 101
+ region: content
settings:
trim_length: 600
third_party_settings: { }
diff --git a/core/modules/responsive_image/src/Tests/ResponsiveImageFieldUiTest.php b/core/modules/responsive_image/src/Tests/ResponsiveImageFieldUiTest.php
index 8b1030f..3d0e32e 100644
--- a/core/modules/responsive_image/src/Tests/ResponsiveImageFieldUiTest.php
+++ b/core/modules/responsive_image/src/Tests/ResponsiveImageFieldUiTest.php
@@ -52,7 +52,11 @@ function testResponsiveImageFormatterUI() {
$this->drupalGet($manage_display);
// Change the formatter and check that the summary is updated.
- $edit = array('fields[field_image][type]' => 'responsive_image', 'refresh_rows' => 'field_image');
+ $edit = array(
+ 'fields[field_image][type]' => 'responsive_image',
+ 'fields[field_image][region]' => 'content',
+ 'refresh_rows' => 'field_image',
+ );
$this->drupalPostAjaxForm(NULL, $edit, array('op' => t('Refresh')));
$this->assertText("Select a responsive image style.", 'The expected summary is displayed.');
diff --git a/core/modules/system/src/Tests/System/DateTimeTest.php b/core/modules/system/src/Tests/System/DateTimeTest.php
index 9ca050f..8e30366 100644
--- a/core/modules/system/src/Tests/System/DateTimeTest.php
+++ b/core/modules/system/src/Tests/System/DateTimeTest.php
@@ -197,6 +197,7 @@ function testEnteringDateTimeViaSelectors() {
$this->drupalGet('admin/structure/types/manage/page_with_date/form-display');
$edit = array(
'fields[field_dt][type]' => 'datetime_datelist',
+ 'fields[field_dt][region]' => 'content',
);
$this->drupalPostForm('admin/structure/types/manage/page_with_date/form-display', $edit, t('Save'));
$this->drupalLogout();
diff --git a/core/modules/system/src/Tests/Update/UpdateEntityDisplayTest.php b/core/modules/system/src/Tests/Update/UpdateEntityDisplayTest.php
new file mode 100644
index 0000000..0a2cbf4
--- /dev/null
+++ b/core/modules/system/src/Tests/Update/UpdateEntityDisplayTest.php
@@ -0,0 +1,49 @@
+databaseDumpFiles = [
+ __DIR__ . '/../../../tests/fixtures/update/drupal-8.bare.standard.php.gz',
+ ];
+ }
+
+ /**
+ * Tests that entity displays are updated with regions for their fields.
+ */
+ public function testUpdate() {
+ // No region key appears pre-update.
+ $entity_form_display = EntityFormDisplay::load('node.article.default');
+ $options = $entity_form_display->getComponent('body');
+ $this->assertFalse(array_key_exists('region', $options));
+
+ $entity_view_display = EntityViewDisplay::load('node.article.default');
+ $options = $entity_view_display->getComponent('body');
+ $this->assertFalse(array_key_exists('region', $options));
+
+ $this->runUpdates();
+
+ // The region key has been populated with 'content'.
+ $entity_form_display = EntityFormDisplay::load('node.article.default');
+ $options = $entity_form_display->getComponent('body');
+ $this->assertIdentical('content', $options['region']);
+
+ $entity_view_display = EntityViewDisplay::load('node.article.default');
+ $options = $entity_view_display->getComponent('body');
+ $this->assertIdentical('content', $options['region']);
+ }
+
+}
diff --git a/core/modules/system/system.post_update.php b/core/modules/system/system.post_update.php
index b75625c..59a49a4 100644
--- a/core/modules/system/system.post_update.php
+++ b/core/modules/system/system.post_update.php
@@ -5,6 +5,10 @@
* Post update functions for System.
*/
+use Drupal\Core\Entity\Display\EntityDisplayInterface;
+use Drupal\Core\Entity\Entity\EntityFormDisplay;
+use Drupal\Core\Entity\Entity\EntityViewDisplay;
+
/**
* @addtogroup updates-8.0.0-beta
* @{
@@ -41,3 +45,15 @@ function system_post_update_recalculate_configuration_entity_dependencies(&$sand
/**
* @} End of "addtogroup updates-8.0.0-beta".
*/
+
+/**
+ * Update entity displays to contain the region for each field.
+ */
+function system_post_update_add_region_to_entity_displays() {
+ $entity_save = function (EntityDisplayInterface $entity) {
+ // preSave() will fill in the correct region based on the 'type'.
+ $entity->save();
+ };
+ array_map($entity_save, EntityViewDisplay::loadMultiple());
+ array_map($entity_save, EntityFormDisplay::loadMultiple());
+}
diff --git a/core/modules/taxonomy/src/Tests/RssTest.php b/core/modules/taxonomy/src/Tests/RssTest.php
index effd4dd..f26cb07 100644
--- a/core/modules/taxonomy/src/Tests/RssTest.php
+++ b/core/modules/taxonomy/src/Tests/RssTest.php
@@ -81,6 +81,7 @@ function testTaxonomyRss() {
$this->drupalGet("admin/structure/types/manage/article/display/rss");
$edit = array(
"fields[taxonomy_" . $this->vocabulary->id() . "][type]" => 'entity_reference_rss_category',
+ "fields[taxonomy_" . $this->vocabulary->id() . "][region]" => 'content',
);
$this->drupalPostForm(NULL, $edit, t('Save'));
diff --git a/core/modules/user/src/Entity/User.php b/core/modules/user/src/Entity/User.php
index 6a3d6b1..062af44 100644
--- a/core/modules/user/src/Entity/User.php
+++ b/core/modules/user/src/Entity/User.php
@@ -438,7 +438,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields['langcode']->setLabel(t('Language code'))
->setDescription(t('The user language code.'))
- ->setDisplayOptions('form', ['type' => 'hidden']);
+ ->setDisplayOptions('form', ['region' => 'hidden']);
$fields['preferred_langcode'] = BaseFieldDefinition::create('language')
->setLabel(t('Preferred language code'))
diff --git a/core/profiles/standard/config/install/core.entity_form_display.block_content.basic.default.yml b/core/profiles/standard/config/install/core.entity_form_display.block_content.basic.default.yml
index ee0c138..7ccb5b0 100644
--- a/core/profiles/standard/config/install/core.entity_form_display.block_content.basic.default.yml
+++ b/core/profiles/standard/config/install/core.entity_form_display.block_content.basic.default.yml
@@ -14,6 +14,7 @@ content:
body:
type: text_textarea_with_summary
weight: -4
+ region: content
settings:
rows: 9
summary_rows: 3
@@ -22,6 +23,7 @@ content:
info:
type: string_textfield
weight: -5
+ region: content
settings:
size: 60
placeholder: ''
diff --git a/core/profiles/standard/config/install/core.entity_form_display.comment.comment.default.yml b/core/profiles/standard/config/install/core.entity_form_display.comment.comment.default.yml
index fa5d834..1010be2 100644
--- a/core/profiles/standard/config/install/core.entity_form_display.comment.comment.default.yml
+++ b/core/profiles/standard/config/install/core.entity_form_display.comment.comment.default.yml
@@ -13,9 +13,11 @@ mode: default
content:
author:
weight: -2
+ region: content
comment_body:
type: text_textarea
weight: 11
+ region: content
settings:
rows: 5
placeholder: ''
@@ -23,6 +25,7 @@ content:
subject:
type: string_textfield
weight: 10
+ region: content
settings:
size: 60
placeholder: ''
diff --git a/core/profiles/standard/config/install/core.entity_form_display.node.article.default.yml b/core/profiles/standard/config/install/core.entity_form_display.node.article.default.yml
index 79156b2..c94e36e 100644
--- a/core/profiles/standard/config/install/core.entity_form_display.node.article.default.yml
+++ b/core/profiles/standard/config/install/core.entity_form_display.node.article.default.yml
@@ -21,6 +21,7 @@ content:
body:
type: text_textarea_with_summary
weight: 1
+ region: content
settings:
rows: 9
summary_rows: 3
@@ -29,16 +30,19 @@ content:
comment:
type: comment_default
weight: 20
+ region: content
settings: { }
third_party_settings: { }
created:
type: datetime_timestamp
weight: 10
+ region: content
settings: { }
third_party_settings: { }
field_image:
type: image_image
weight: 4
+ region: content
settings:
progress_indicator: throbber
preview_image_style: thumbnail
@@ -46,11 +50,13 @@ content:
field_tags:
type: entity_reference_autocomplete_tags
weight: 3
+ region: content
settings: { }
third_party_settings: { }
path:
type: path
weight: 30
+ region: content
settings: { }
third_party_settings: { }
promote:
@@ -58,16 +64,19 @@ content:
settings:
display_label: true
weight: 15
+ region: content
third_party_settings: { }
sticky:
type: boolean_checkbox
settings:
display_label: true
weight: 16
+ region: content
third_party_settings: { }
title:
type: string_textfield
weight: 0
+ region: content
settings:
size: 60
placeholder: ''
@@ -75,6 +84,7 @@ content:
uid:
type: entity_reference_autocomplete
weight: 5
+ region: content
settings:
match_operator: CONTAINS
size: 60
diff --git a/core/profiles/standard/config/install/core.entity_form_display.node.page.default.yml b/core/profiles/standard/config/install/core.entity_form_display.node.page.default.yml
index 1fef06d..0b7ffd1 100644
--- a/core/profiles/standard/config/install/core.entity_form_display.node.page.default.yml
+++ b/core/profiles/standard/config/install/core.entity_form_display.node.page.default.yml
@@ -15,6 +15,7 @@ content:
body:
type: text_textarea_with_summary
weight: 31
+ region: content
settings:
rows: 9
summary_rows: 3
@@ -23,11 +24,13 @@ content:
created:
type: datetime_timestamp
weight: 10
+ region: content
settings: { }
third_party_settings: { }
path:
type: path
weight: 30
+ region: content
settings: { }
third_party_settings: { }
promote:
@@ -35,16 +38,19 @@ content:
settings:
display_label: true
weight: 15
+ region: content
third_party_settings: { }
sticky:
type: boolean_checkbox
settings:
display_label: true
weight: 16
+ region: content
third_party_settings: { }
title:
type: string_textfield
weight: -5
+ region: content
settings:
size: 60
placeholder: ''
@@ -52,6 +58,7 @@ content:
uid:
type: entity_reference_autocomplete
weight: 5
+ region: content
settings:
match_operator: CONTAINS
size: 60
diff --git a/core/profiles/standard/config/install/core.entity_form_display.user.user.default.yml b/core/profiles/standard/config/install/core.entity_form_display.user.user.default.yml
index 466b6e0..6832229 100644
--- a/core/profiles/standard/config/install/core.entity_form_display.user.user.default.yml
+++ b/core/profiles/standard/config/install/core.entity_form_display.user.user.default.yml
@@ -14,12 +14,16 @@ mode: default
content:
account:
weight: -10
+ region: content
contact:
weight: 5
+ region: content
language:
weight: 0
+ region: content
timezone:
weight: 6
+ region: content
user_picture:
type: image_image
settings:
@@ -27,4 +31,5 @@ content:
preview_image_style: thumbnail
third_party_settings: { }
weight: -1
+ region: content
hidden: { }
diff --git a/core/profiles/standard/config/install/core.entity_view_display.block_content.basic.default.yml b/core/profiles/standard/config/install/core.entity_view_display.block_content.basic.default.yml
index bd52f77..e494882 100644
--- a/core/profiles/standard/config/install/core.entity_view_display.block_content.basic.default.yml
+++ b/core/profiles/standard/config/install/core.entity_view_display.block_content.basic.default.yml
@@ -15,6 +15,7 @@ content:
label: hidden
type: text_default
weight: 0
+ region: content
settings: { }
third_party_settings: { }
hidden: { }
diff --git a/core/profiles/standard/config/install/core.entity_view_display.comment.comment.default.yml b/core/profiles/standard/config/install/core.entity_view_display.comment.comment.default.yml
index 1ed49ce..6ae213d 100644
--- a/core/profiles/standard/config/install/core.entity_view_display.comment.comment.default.yml
+++ b/core/profiles/standard/config/install/core.entity_view_display.comment.comment.default.yml
@@ -15,8 +15,10 @@ content:
label: hidden
type: text_default
weight: 0
+ region: content
settings: { }
third_party_settings: { }
links:
weight: 100
+ region: content
hidden: { }
diff --git a/core/profiles/standard/config/install/core.entity_view_display.node.article.default.yml b/core/profiles/standard/config/install/core.entity_view_display.node.article.default.yml
index 98a2de8..5c43252 100644
--- a/core/profiles/standard/config/install/core.entity_view_display.node.article.default.yml
+++ b/core/profiles/standard/config/install/core.entity_view_display.node.article.default.yml
@@ -22,12 +22,14 @@ content:
body:
type: text_default
weight: 0
+ region: content
settings: { }
third_party_settings: { }
label: hidden
comment:
type: comment_default
weight: 110
+ region: content
label: above
settings:
view_mode: default
@@ -36,6 +38,7 @@ content:
field_image:
type: image
weight: -1
+ region: content
settings:
image_style: large
image_link: ''
@@ -44,12 +47,14 @@ content:
field_tags:
type: entity_reference_label
weight: 10
+ region: content
label: above
settings:
link: true
third_party_settings: { }
links:
weight: 100
+ region: content
hidden:
field_image: true
field_tags: true
diff --git a/core/profiles/standard/config/install/core.entity_view_display.node.article.rss.yml b/core/profiles/standard/config/install/core.entity_view_display.node.article.rss.yml
index 75a14a3..84660b6 100644
--- a/core/profiles/standard/config/install/core.entity_view_display.node.article.rss.yml
+++ b/core/profiles/standard/config/install/core.entity_view_display.node.article.rss.yml
@@ -17,6 +17,7 @@ mode: rss
content:
links:
weight: 100
+ region: content
hidden:
body: true
comment: true
diff --git a/core/profiles/standard/config/install/core.entity_view_display.node.article.teaser.yml b/core/profiles/standard/config/install/core.entity_view_display.node.article.teaser.yml
index 43ee079..7b96908 100644
--- a/core/profiles/standard/config/install/core.entity_view_display.node.article.teaser.yml
+++ b/core/profiles/standard/config/install/core.entity_view_display.node.article.teaser.yml
@@ -21,6 +21,7 @@ content:
body:
type: text_summary_or_trimmed
weight: 0
+ region: content
settings:
trim_length: 600
third_party_settings: { }
@@ -28,6 +29,7 @@ content:
field_image:
type: image
weight: -1
+ region: content
settings:
image_style: medium
image_link: content
@@ -36,12 +38,14 @@ content:
field_tags:
type: entity_reference_label
weight: 10
+ region: content
settings:
link: true
third_party_settings: { }
label: above
links:
weight: 100
+ region: content
hidden:
comment: true
field_image: true
diff --git a/core/profiles/standard/config/install/core.entity_view_display.node.page.default.yml b/core/profiles/standard/config/install/core.entity_view_display.node.page.default.yml
index dcb2d3e..8afd942 100644
--- a/core/profiles/standard/config/install/core.entity_view_display.node.page.default.yml
+++ b/core/profiles/standard/config/install/core.entity_view_display.node.page.default.yml
@@ -16,8 +16,10 @@ content:
label: hidden
type: text_default
weight: 100
+ region: content
settings: { }
third_party_settings: { }
links:
weight: 101
+ region: content
hidden: { }
diff --git a/core/profiles/standard/config/install/core.entity_view_display.node.page.teaser.yml b/core/profiles/standard/config/install/core.entity_view_display.node.page.teaser.yml
index f235a10..bc7a68c 100644
--- a/core/profiles/standard/config/install/core.entity_view_display.node.page.teaser.yml
+++ b/core/profiles/standard/config/install/core.entity_view_display.node.page.teaser.yml
@@ -17,9 +17,11 @@ content:
label: hidden
type: text_summary_or_trimmed
weight: 100
+ region: content
settings:
trim_length: 600
third_party_settings: { }
links:
weight: 101
+ region: content
hidden: { }
diff --git a/core/profiles/standard/config/install/core.entity_view_display.user.user.compact.yml b/core/profiles/standard/config/install/core.entity_view_display.user.user.compact.yml
index 4c13792..2ff13ad 100644
--- a/core/profiles/standard/config/install/core.entity_view_display.user.user.compact.yml
+++ b/core/profiles/standard/config/install/core.entity_view_display.user.user.compact.yml
@@ -16,6 +16,7 @@ content:
user_picture:
type: image
weight: 0
+ region: content
settings:
image_style: thumbnail
image_link: content
diff --git a/core/profiles/standard/config/install/core.entity_view_display.user.user.default.yml b/core/profiles/standard/config/install/core.entity_view_display.user.user.default.yml
index 9e4621d..ef1fdd7 100644
--- a/core/profiles/standard/config/install/core.entity_view_display.user.user.default.yml
+++ b/core/profiles/standard/config/install/core.entity_view_display.user.user.default.yml
@@ -14,9 +14,11 @@ mode: default
content:
member_for:
weight: 5
+ region: content
user_picture:
type: image
weight: 0
+ region: content
settings:
image_style: thumbnail
image_link: content
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityDisplayBaseTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityDisplayBaseTest.php
new file mode 100644
index 0000000..137201a
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityDisplayBaseTest.php
@@ -0,0 +1,57 @@
+ 'entity_test',
+ 'bundle' => 'entity_test',
+ 'mode' => 'default',
+ 'status' => TRUE,
+ 'content' => [
+ 'foo' => ['type' => 'visible'],
+ 'bar' => ['type' => 'hidden'],
+ 'name' => ['type' => 'hidden', 'region' => 'content'],
+ ],
+ ]);
+
+ // Ensure that no region is set on the component.
+ $this->assertArrayNotHasKey('region', $entity_display->getComponent('foo'));
+ $this->assertArrayNotHasKey('region', $entity_display->getComponent('bar'));
+
+ // Ensure that a region is set on the component after saving.
+ $entity_display->save();
+
+ // The component with a visible type has been assigned a region.
+ $component = $entity_display->getComponent('foo');
+ $this->assertArrayHasKey('region', $component);
+ $this->assertSame('content', $component['region']);
+
+ // The component with a hidden type has been removed.
+ $this->assertNull($entity_display->getComponent('bar'));
+
+ // The component with a valid region and hidden type is unchanged.
+ $component = $entity_display->getComponent('name');
+ $this->assertArrayHasKey('region', $component);
+ $this->assertSame('content', $component['region']);
+ }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityDisplayFormBaseTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityDisplayFormBaseTest.php
new file mode 100644
index 0000000..6ec6883
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityDisplayFormBaseTest.php
@@ -0,0 +1,145 @@
+prophesize(EntityDisplayInterface::class);
+ $entity->getPluginCollections()->willReturn([]);
+ $entity->getTargetEntityTypeId()->willReturn('entity_test_with_bundle');
+ $entity->getTargetBundle()->willReturn('target_bundle');
+
+ // An initially hidden field, with a submitted region change.
+ $entity->getComponent('new_field_mismatch_type_visible')->willReturn([]);
+ $field_values['new_field_mismatch_type_visible'] = [
+ 'weight' => 0,
+ 'type' => 'textfield',
+ 'region' => 'hidden',
+ ];
+ $entity->removeComponent('new_field_mismatch_type_visible')
+ ->will(function ($args) {
+ // On subsequent calls, getComponent() will return an empty array.
+ $this->getComponent($args[0])->willReturn([]);
+ })
+ ->shouldBeCalled();
+
+ // An initially visible field, with identical submitted values.
+ $entity->getComponent('field_visible_no_changes')
+ ->willReturn([
+ 'weight' => 0,
+ 'type' => 'textfield',
+ 'region' => 'content',
+ ]);
+ $field_values['field_visible_no_changes'] = [
+ 'weight' => 0,
+ 'type' => 'textfield',
+ 'region' => 'content',
+ ];
+ $entity
+ ->setComponent('field_visible_no_changes', [
+ 'weight' => 0,
+ 'type' => 'textfield',
+ 'region' => 'content',
+ ])
+ ->shouldBeCalled();
+
+
+ // An initially visible field, with a submitted region change.
+ $entity->getComponent('field_start_visible_change_region')
+ ->willReturn([
+ 'weight' => 0,
+ 'type' => 'textfield',
+ 'region' => 'content',
+ ]);
+ $field_values['field_start_visible_change_region'] = [
+ 'weight' => 0,
+ 'type' => 'textfield',
+ 'region' => 'hidden',
+ ];
+ $entity->removeComponent('field_start_visible_change_region')
+ ->will(function ($args) {
+ // On subsequent calls, getComponent() will return an empty array.
+ $this->getComponent($args[0])->willReturn([]);
+ })
+ ->shouldBeCalled();
+
+ // A field that is flagged for plugin settings update on the second build.
+ $entity->getComponent('field_plugin_settings_update')
+ ->willReturn([
+ 'weight' => 0,
+ 'type' => 'textfield',
+ 'region' => 'content',
+ ]);
+ $field_values['field_plugin_settings_update'] = [
+ 'weight' => 0,
+ 'type' => 'textfield',
+ 'region' => 'content',
+ 'settings_edit_form' => [
+ 'third_party_settings' => [
+ 'foo' => 'bar',
+ ],
+ ],
+ ];
+ $entity
+ ->setComponent('field_plugin_settings_update', [
+ 'weight' => 0,
+ 'type' => 'textfield',
+ 'region' => 'content',
+ ])
+ ->will(function ($args) {
+ // On subsequent calls, getComponent() will return the newly set values.
+ $this->getComponent($args[0])->willReturn($args[1]);
+ $args[1] += [
+ 'settings' => [],
+ 'third_party_settings' => [
+ 'foo' => 'bar',
+ ],
+ ];
+ $this->setComponent($args[0], $args[1])->shouldBeCalled();
+ })
+ ->shouldBeCalled();
+
+ $form_object = new EntityViewDisplayEditForm($this->container->get('plugin.manager.field.field_type'), $this->container->get('plugin.manager.field.formatter'));
+ $form_object->setEntityManager($this->container->get('entity.manager'));
+ $form_object->setEntity($entity->reveal());
+
+ $form = [
+ '#fields' => array_keys($field_values),
+ '#extra' => [],
+ ];
+ $form_state = new FormState();
+ $form_state->setValues(['fields' => $field_values]);
+ $form_state->setProcessInput();
+
+ $form_object->buildEntity($form, $form_state);
+ $form_state->setSubmitted();
+
+ // Flag one field for updating plugin settings.
+ $form_state->set('plugin_settings_update', 'field_plugin_settings_update');
+ // During form submission, buildEntity() will be called twice. Simulate that
+ // here to prove copyFormValuesToEntity() is idempotent.
+ $form_object->buildEntity($form, $form_state);
+ }
+
+}