diff --git a/core/composer.json b/core/composer.json
index 7ae0ccf..2f9c959 100644
--- a/core/composer.json
+++ b/core/composer.json
@@ -95,6 +95,7 @@
"drupal/editor": "self.version",
"drupal/entity_reference": "self.version",
"drupal/field": "self.version",
+ "drupal/field_layout": "self.version",
"drupal/field_ui": "self.version",
"drupal/file": "self.version",
"drupal/filter": "self.version",
@@ -105,6 +106,7 @@
"drupal/image": "self.version",
"drupal/inline_form_errors": "self.version",
"drupal/language": "self.version",
+ "drupal/layout_discovery": "self.version",
"drupal/link": "self.version",
"drupal/locale": "self.version",
"drupal/minimal": "self.version",
diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml
index 73db361..0fbeeed 100644
--- a/core/config/schema/core.data_types.schema.yml
+++ b/core/config/schema/core.data_types.schema.yml
@@ -361,6 +361,13 @@ display_variant.plugin:
type: string
label: 'UUID'
+layout_plugin.settings:
+ type: mapping
+ label: 'Layout settings'
+
+layout_plugin.settings.*:
+ type: layout_plugin.settings
+
base_entity_reference_field_settings:
type: mapping
mapping:
diff --git a/core/core.api.php b/core/core.api.php
index eb16ee0..ac0e385 100644
--- a/core/core.api.php
+++ b/core/core.api.php
@@ -2179,6 +2179,17 @@ function hook_display_variant_plugin_alter(array &$definitions) {
}
/**
+ * Allow modules to alter layout plugin definitions.
+ *
+ * @param \Drupal\Core\Layout\LayoutDefinition[] $definitions
+ * The array of layout definitions, keyed by plugin ID.
+ */
+function hook_layout_alter(&$definitions) {
+ // Remove a layout.
+ unset($definitions['twocol']);
+}
+
+/**
* Flush all persistent and static caches.
*
* This hook asks your module to clear all of its static caches,
diff --git a/core/lib/Drupal/Core/Layout/Annotation/Layout.php b/core/lib/Drupal/Core/Layout/Annotation/Layout.php
new file mode 100644
index 0000000..1cb5ff0
--- /dev/null
+++ b/core/lib/Drupal/Core/Layout/Annotation/Layout.php
@@ -0,0 +1,157 @@
+definition);
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Layout/DerivablePluginDefinitionInterface.php b/core/lib/Drupal/Core/Layout/DerivablePluginDefinitionInterface.php
new file mode 100644
index 0000000..3d24b9d
--- /dev/null
+++ b/core/lib/Drupal/Core/Layout/DerivablePluginDefinitionInterface.php
@@ -0,0 +1,42 @@
+setConfiguration($configuration);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function build(array $regions) {
+ $build = 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\Core\Layout\LayoutDefinition
+ */
+ public function getPluginDefinition() {
+ return parent::getPluginDefinition();
+ }
+
+}
diff --git a/core/lib/Drupal/Core/Layout/LayoutDefinition.php b/core/lib/Drupal/Core/Layout/LayoutDefinition.php
new file mode 100644
index 0000000..9fab205
--- /dev/null
+++ b/core/lib/Drupal/Core/Layout/LayoutDefinition.php
@@ -0,0 +1,552 @@
+ $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;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setClass($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/lib/Drupal/Core/Layout/LayoutInterface.php b/core/lib/Drupal/Core/Layout/LayoutInterface.php
new file mode 100644
index 0000000..bb60df0
--- /dev/null
+++ b/core/lib/Drupal/Core/Layout/LayoutInterface.php
@@ -0,0 +1,38 @@
+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.
+ // @todo Remove this once https://www.drupal.org/node/2824655 is resolved.
+ $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);
+ // Prepend 'layout__' so the base theme hook will be used.
+ $definition->setThemeHook('layout__' . strtr($template, '-', '_'));
+ $definition->setTemplatePath($template_path);
+ }
+
+ if (!$definition->getDefaultRegion()) {
+ $definition->setDefaultRegion(key($definition->getRegions()));
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getThemeImplementations() {
+ $hooks = [];
+ $hooks['layout'] = [
+ 'render element' => 'content',
+ ];
+ /** @var \Drupal\Core\Layout\LayoutDefinition[] $definitions */
+ $definitions = $this->getDefinitions();
+ foreach ($definitions as $definition) {
+ if ($template = $definition->getTemplate()) {
+ $hooks[$definition->getThemeHook()] = [
+ 'render element' => 'content',
+ 'base hook' => '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\Core\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\Core\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/lib/Drupal/Core/Layout/LayoutPluginManagerInterface.php b/core/lib/Drupal/Core/Layout/LayoutPluginManagerInterface.php
new file mode 100644
index 0000000..df15be0
--- /dev/null
+++ b/core/lib/Drupal/Core/Layout/LayoutPluginManagerInterface.php
@@ -0,0 +1,70 @@
+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/lib/Drupal/Core/Layout/ObjectDefinitionDiscoveryDecorator.php b/core/lib/Drupal/Core/Layout/ObjectDefinitionDiscoveryDecorator.php
new file mode 100644
index 0000000..e7a6071
--- /dev/null
+++ b/core/lib/Drupal/Core/Layout/ObjectDefinitionDiscoveryDecorator.php
@@ -0,0 +1,81 @@
+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/field_layout/config/schema/field_layout.schema.yml b/core/modules/field_layout/config/schema/field_layout.schema.yml
new file mode 100644
index 0000000..3b53b3f
--- /dev/null
+++ b/core/modules/field_layout/config/schema/field_layout.schema.yml
@@ -0,0 +1,16 @@
+core.entity_view_display.*.*.*.third_party.field_layout:
+ type: field_layout.third_party_settings
+
+core.entity_form_display.*.*.*.third_party.field_layout:
+ type: field_layout.third_party_settings
+
+field_layout.third_party_settings:
+ type: mapping
+ label: 'Per view mode field layout settings'
+ mapping:
+ id:
+ type: string
+ label: 'Layout ID'
+ settings:
+ type: layout_plugin.settings.[%parent.id]
+ label: 'Layout settings'
diff --git a/core/modules/field_layout/field_layout.info.yml b/core/modules/field_layout/field_layout.info.yml
new file mode 100644
index 0000000..237f18d
--- /dev/null
+++ b/core/modules/field_layout/field_layout.info.yml
@@ -0,0 +1,8 @@
+name: 'Field Layout'
+type: module
+description: 'Adds layout capabilities to the Field UI.'
+package: Core (Experimental)
+version: VERSION
+core: 8.x
+dependencies:
+ - layout_discovery
diff --git a/core/modules/field_layout/field_layout.install b/core/modules/field_layout/field_layout.install
new file mode 100644
index 0000000..6f94674
--- /dev/null
+++ b/core/modules/field_layout/field_layout.install
@@ -0,0 +1,26 @@
+save();
+ };
+ array_map($entity_save, EntityViewDisplay::loadMultiple());
+ array_map($entity_save, EntityFormDisplay::loadMultiple());
+
+ // Invalidate the render cache since all content will now have a layout.
+ Cache::invalidateTags(['rendered']);
+}
diff --git a/core/modules/field_layout/field_layout.layouts.yml b/core/modules/field_layout/field_layout.layouts.yml
new file mode 100644
index 0000000..b8c6ef3
--- /dev/null
+++ b/core/modules/field_layout/field_layout.layouts.yml
@@ -0,0 +1,21 @@
+onecol:
+ label: 'One column'
+ path: layouts/onecol
+ template: field-layout--onecol
+ category: 'Columns: 1'
+ default_region: content
+ regions:
+ content:
+ label: Content
+twocol:
+ label: 'Two column'
+ path: layouts/twocol
+ template: field-layout--twocol
+ library: field_layout/drupal.field_layout.twocol
+ category: 'Columns: 2'
+ default_region: left
+ regions:
+ left:
+ label: Left
+ right:
+ label: Right
diff --git a/core/modules/field_layout/field_layout.libraries.yml b/core/modules/field_layout/field_layout.libraries.yml
new file mode 100644
index 0000000..d87df5e
--- /dev/null
+++ b/core/modules/field_layout/field_layout.libraries.yml
@@ -0,0 +1,5 @@
+drupal.field_layout.twocol:
+ version: VERSION
+ css:
+ layout:
+ layouts/twocol/twocol.layout.css: {}
diff --git a/core/modules/field_layout/field_layout.module b/core/modules/field_layout/field_layout.module
new file mode 100644
index 0000000..11dcdc1
--- /dev/null
+++ b/core/modules/field_layout/field_layout.module
@@ -0,0 +1,69 @@
+' . t('About') . '';
+ $output .= '
' . t('The Field Layout module allows you to arrange fields into regions on forms and displays of entities such as nodes and users.') . '
';
+ $output .= '
' . t('For more information, see the online documentation for the Field Layout module.', [':field-layout-documentation' => 'https://www.drupal.org/documentation/modules/@todo_once_module_name_is_decided_upon']) . '
';
+ return $output;
+ }
+}
+
+/**
+ * Implements hook_entity_type_alter().
+ */
+function field_layout_entity_type_alter(array &$entity_types) {
+ /** @var $entity_types \Drupal\Core\Entity\EntityTypeInterface[] */
+ $entity_types['entity_view_display']->setClass(FieldLayoutEntityViewDisplay::class);
+ $entity_types['entity_form_display']->setClass(FieldLayoutEntityFormDisplay::class);
+
+ // The form classes are only needed when Field UI is installed.
+ if (\Drupal::moduleHandler()->moduleExists('field_ui')) {
+ $entity_types['entity_view_display']->setFormClass('edit', FieldLayoutEntityViewDisplayEditForm::class);
+ $entity_types['entity_form_display']->setFormClass('edit', FieldLayoutEntityFormDisplayEditForm::class);
+ }
+}
+
+/**
+ * Implements hook_entity_view_alter().
+ */
+function field_layout_entity_view_alter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) {
+ if ($display instanceof EntityDisplayWithLayoutInterface) {
+ \Drupal::classResolver()->getInstanceFromDefinition(FieldLayoutBuilder::class)
+ ->build($build, $display, 'view');
+ }
+}
+
+/**
+ * Implements hook_form_alter().
+ */
+function field_layout_form_alter(&$form, FormStateInterface $form_state, $form_id) {
+ $form_object = $form_state->getFormObject();
+ if ($form_object instanceof ContentEntityFormInterface && $display = $form_object->getFormDisplay($form_state)) {
+ if ($display instanceof EntityDisplayWithLayoutInterface) {
+ \Drupal::classResolver()->getInstanceFromDefinition(FieldLayoutBuilder::class)
+ ->build($form, $display, 'form');
+ }
+ }
+}
diff --git a/core/modules/field_layout/layouts/onecol/field-layout--onecol.html.twig b/core/modules/field_layout/layouts/onecol/field-layout--onecol.html.twig
new file mode 100644
index 0000000..cce4893
--- /dev/null
+++ b/core/modules/field_layout/layouts/onecol/field-layout--onecol.html.twig
@@ -0,0 +1,24 @@
+{#
+/**
+ * @file
+ * Default theme implementation to display a one column layout.
+ *
+ * Available variables:
+ * - content: The content for this layout.
+ * - attributes: HTML attributes for the layout