diff --git a/core/composer.json b/core/composer.json index 53ab8fa..c52add0 100644 --- a/core/composer.json +++ b/core/composer.json @@ -94,6 +94,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", @@ -104,6 +105,7 @@ "drupal/image": "self.version", "drupal/inline_form_errors": "self.version", "drupal/language": "self.version", + "drupal/layout_plugin": "self.version", "drupal/link": "self.version", "drupal/locale": "self.version", "drupal/minimal": "self.version", diff --git a/core/config/schema/core.entity.schema.yml b/core/config/schema/core.entity.schema.yml index bf0e12d..711ca0f 100644 --- a/core/config/schema/core.entity.schema.yml +++ b/core/config/schema/core.entity.schema.yml @@ -64,6 +64,9 @@ core.entity_view_display.*.*.*: weight: type: integer label: 'Weight' + region: + type: string + label: 'Region' label: type: string label: 'Label setting machine name' @@ -115,6 +118,9 @@ core.entity_form_display.*.*.*: weight: type: integer label: 'Weight' + region: + type: string + label: 'Region' settings: type: field.widget.settings.[%parent.type] label: 'Settings' diff --git a/core/includes/entity.inc b/core/includes/entity.inc index b0e5088..e8419fb 100644 --- a/core/includes/entity.inc +++ b/core/includes/entity.inc @@ -514,7 +514,7 @@ function entity_get_display($entity_type, $bundle, $view_mode) { * 'weight' => 1, * )) * ->setComponent('field_image', array( - * 'type' => 'hidden', + * 'region' => 'hidden', * )) * ->save(); * @endcode diff --git a/core/lib/Drupal/Core/Entity/ContentEntityBase.php b/core/lib/Drupal/Core/Entity/ContentEntityBase.php index a915c07..1d69be2 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityBase.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php @@ -1171,7 +1171,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { $fields[$entity_type->getKey('langcode')] = BaseFieldDefinition::create('language') ->setLabel(new TranslatableMarkup('Language')) ->setDisplayOptions('view', [ - 'type' => 'hidden', + 'region' => 'hidden', ]) ->setDisplayOptions('form', [ 'type' => 'language_select', diff --git a/core/lib/Drupal/Core/Entity/EntityDisplayBase.php b/core/lib/Drupal/Core/Entity/EntityDisplayBase.php index 4de891a..eddcb5e 100644 --- a/core/lib/Drupal/Core/Entity/EntityDisplayBase.php +++ b/core/lib/Drupal/Core/Entity/EntityDisplayBase.php @@ -154,6 +154,7 @@ public function __construct(array $values, $entity_type) { protected function init() { // Only populate defaults for "official" view modes and form modes. if ($this->mode !== static::CUSTOM_MODE) { + $default_region = $this->getDefaultRegion(); // Fill in defaults for extra fields. $context = $this->displayContext == 'view' ? 'display' : $this->displayContext; $extra_fields = \Drupal::entityManager()->getExtraFields($this->targetEntityType, $this->bundle); @@ -170,6 +171,10 @@ protected function init() { $this->hidden[$name] = TRUE; } } + // Ensure extra fields have a 'region'. + if (isset($this->content[$name])) { + $this->content[$name] += ['region' => $default_region]; + } } // Fill in defaults for fields. @@ -178,10 +183,17 @@ protected function init() { if (!$definition->isDisplayConfigurable($this->displayContext) || (!isset($this->content[$name]) && !isset($this->hidden[$name]))) { $options = $definition->getDisplayOptions($this->displayContext); - if (!empty($options['type']) && $options['type'] == 'hidden') { + // @todo Remove handling of 'type' in https://www.drupal.org/node/2799641. + if (!isset($options['region']) && !empty($options['type']) && $options['type'] === 'hidden') { + $options['region'] = 'hidden'; + @trigger_error("Specifying 'type' => 'hidden' is deprecated, use 'region' => 'hidden' instead.", E_USER_DEPRECATED); + } + + if (!empty($options['region']) && $options['region'] === 'hidden') { $this->hidden[$name] = TRUE; } elseif ($options) { + $options += ['region' => $default_region]; $this->content[$name] = $this->pluginManager->prepareConfiguration($definition->getType(), $options); } // Note: (base) fields that do not specify display options are not @@ -239,12 +251,40 @@ public function id() { * {@inheritdoc} */ public function preSave(EntityStorageInterface $storage, $update = TRUE) { + // Ensure that a region is set on each component. + foreach ($this->getComponents() as $name => $component) { + $this->handleHiddenType($name, $component); + // Ensure that a region is set. + if (isset($this->content[$name]) && !isset($component['region'])) { + // Directly set the component to bypass other changes in setComponent(). + $this->content[$name]['region'] = $this->getDefaultRegion(); + } + } + ksort($this->content); ksort($this->hidden); parent::preSave($storage, $update); } /** + * Handles a component type of 'hidden'. + * + * @deprecated This method exists only for backwards compatibility. + * + * @todo Remove this in https://www.drupal.org/node/2799641. + * + * @param string $name + * The name of the component. + * @param array $component + * The component array. + */ + protected function handleHiddenType($name, array $component) { + if (!isset($component['region']) && isset($component['type']) && $component['type'] === 'hidden') { + $this->removeComponent($name); + } + } + + /** * {@inheritdoc} */ public function calculateDependencies() { @@ -505,6 +545,16 @@ protected function getPluginRemovedDependencies(array $plugin_dependencies, arra } /** + * Gets the default region. + * + * @return string + * The default region for this display. + */ + protected function getDefaultRegion() { + return 'content'; + } + + /** * {@inheritdoc} */ public function __sleep() { diff --git a/core/lib/Drupal/Core/Entity/entity.api.php b/core/lib/Drupal/Core/Entity/entity.api.php index 60ae741..8aaec20 100644 --- a/core/lib/Drupal/Core/Entity/entity.api.php +++ b/core/lib/Drupal/Core/Entity/entity.api.php @@ -1629,7 +1629,7 @@ function hook_entity_form_display_alter(\Drupal\Core\Entity\Display\EntityFormDi // Hide the 'user_picture' field from the register form. if ($context['entity_type'] == 'user' && $context['form_mode'] == 'register') { $form_display->setComponent('user_picture', array( - 'type' => 'hidden', + 'region' => 'hidden', )); } } diff --git a/core/lib/Drupal/Core/Field/BaseFieldDefinition.php b/core/lib/Drupal/Core/Field/BaseFieldDefinition.php index 594f31d..e28d997 100644 --- a/core/lib/Drupal/Core/Field/BaseFieldDefinition.php +++ b/core/lib/Drupal/Core/Field/BaseFieldDefinition.php @@ -414,7 +414,7 @@ public function setDisplayOptions($display_context, array $options) { public function setDisplayConfigurable($display_context, $configurable) { // If no explicit display options have been specified, default to 'hidden'. if (empty($this->definition['display'][$display_context])) { - $this->definition['display'][$display_context]['options'] = array('type' => 'hidden'); + $this->definition['display'][$display_context]['options'] = array('region' => 'hidden'); } $this->definition['display'][$display_context]['configurable'] = $configurable; return $this; diff --git a/core/lib/Drupal/Core/Field/FieldDefinitionInterface.php b/core/lib/Drupal/Core/Field/FieldDefinitionInterface.php index a7b613b..dbfda44 100644 --- a/core/lib/Drupal/Core/Field/FieldDefinitionInterface.php +++ b/core/lib/Drupal/Core/Field/FieldDefinitionInterface.php @@ -137,10 +137,13 @@ public function isDisplayConfigurable($display_context); * - label: (string) Position of the field label. The default 'field' theme * implementation supports the values 'inline', 'above' and 'hidden'. * Defaults to 'above'. Only applies to 'view' context. + * - region: (string) The region the field is in, or 'hidden'. If not + * specified, the default region will be used. * - type: (string) The plugin (widget or formatter depending on - * $display_context) to use, or 'hidden'. If not specified or if the - * requested plugin is unknown, the 'default_widget' / 'default_formatter' - * for the field type will be used. + * $display_context) to use. If not specified or if the requested plugin + * is unknown, the 'default_widget' / 'default_formatter' for the field + * type will be used. Previously 'hidden' was a valid value, it is now + * deprecated in favor of specifying 'region' => 'hidden'. * - settings: (array) Settings for the plugin specified above. The default * settings for the plugin will be used for settings left unspecified. * - third_party_settings: (array) Settings provided by other extensions diff --git a/core/modules/aggregator/config/install/core.entity_view_display.aggregator_feed.aggregator_feed.default.yml b/core/modules/aggregator/config/install/core.entity_view_display.aggregator_feed.aggregator_feed.default.yml index e3c23cf..e0232cf 100644 --- a/core/modules/aggregator/config/install/core.entity_view_display.aggregator_feed.aggregator_feed.default.yml +++ b/core/modules/aggregator/config/install/core.entity_view_display.aggregator_feed.aggregator_feed.default.yml @@ -11,20 +11,26 @@ content: checked: type: timestamp_ago weight: 1 + region: content settings: { } third_party_settings: { } label: inline description: weight: 3 + region: content feed_icon: weight: 5 + region: content image: weight: 2 + region: content items: weight: 0 + region: content link: type: uri_link weight: 4 + region: content settings: { } third_party_settings: { } label: inline diff --git a/core/modules/aggregator/config/install/core.entity_view_display.aggregator_feed.aggregator_feed.summary.yml b/core/modules/aggregator/config/install/core.entity_view_display.aggregator_feed.aggregator_feed.summary.yml index 40425f2..5e5e468 100644 --- a/core/modules/aggregator/config/install/core.entity_view_display.aggregator_feed.aggregator_feed.summary.yml +++ b/core/modules/aggregator/config/install/core.entity_view_display.aggregator_feed.aggregator_feed.summary.yml @@ -12,8 +12,10 @@ mode: summary content: items: weight: 0 + region: content more_link: weight: 1 + region: content hidden: checked: true description: true diff --git a/core/modules/aggregator/config/install/core.entity_view_display.aggregator_item.aggregator_item.summary.yml b/core/modules/aggregator/config/install/core.entity_view_display.aggregator_item.aggregator_item.summary.yml index 837bee0..8e29395 100644 --- a/core/modules/aggregator/config/install/core.entity_view_display.aggregator_item.aggregator_item.summary.yml +++ b/core/modules/aggregator/config/install/core.entity_view_display.aggregator_item.aggregator_item.summary.yml @@ -12,6 +12,7 @@ mode: summary content: timestamp: weight: 0 + region: content hidden: author: true description: true diff --git a/core/modules/aggregator/src/Entity/Item.php b/core/modules/aggregator/src/Entity/Item.php index 2a2c195..969e4e4 100644 --- a/core/modules/aggregator/src/Entity/Item.php +++ b/core/modules/aggregator/src/Entity/Item.php @@ -76,7 +76,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setLabel(t('Link')) ->setDescription(t('The link of the feed item.')) ->setDisplayOptions('view', array( - 'type' => 'hidden', + 'region' => 'hidden', )) ->setDisplayConfigurable('view', TRUE); diff --git a/core/modules/book/config/install/core.entity_form_display.node.book.default.yml b/core/modules/book/config/install/core.entity_form_display.node.book.default.yml index 1ec4eb1..58aba45 100644 --- a/core/modules/book/config/install/core.entity_form_display.node.book.default.yml +++ b/core/modules/book/config/install/core.entity_form_display.node.book.default.yml @@ -14,6 +14,7 @@ content: body: type: text_textarea_with_summary weight: 26 + region: content settings: rows: 9 summary_rows: 3 @@ -22,6 +23,7 @@ content: created: type: datetime_timestamp weight: 10 + region: content settings: { } third_party_settings: { } promote: @@ -29,16 +31,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: '' @@ -46,6 +51,7 @@ content: uid: type: entity_reference_autocomplete weight: 5 + region: content settings: match_operator: CONTAINS size: 60 diff --git a/core/modules/book/config/install/core.entity_view_display.node.book.default.yml b/core/modules/book/config/install/core.entity_view_display.node.book.default.yml index 729516e..d6ef64d 100644 --- a/core/modules/book/config/install/core.entity_view_display.node.book.default.yml +++ b/core/modules/book/config/install/core.entity_view_display.node.book.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/modules/book/config/install/core.entity_view_display.node.book.teaser.yml b/core/modules/book/config/install/core.entity_view_display.node.book.teaser.yml index fb22db6..77a62c3 100644 --- a/core/modules/book/config/install/core.entity_view_display.node.book.teaser.yml +++ b/core/modules/book/config/install/core.entity_view_display.node.book.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/modules/content_moderation/src/EntityTypeInfo.php b/core/modules/content_moderation/src/EntityTypeInfo.php index ab7e918..9a0d2d3 100644 --- a/core/modules/content_moderation/src/EntityTypeInfo.php +++ b/core/modules/content_moderation/src/EntityTypeInfo.php @@ -299,7 +299,7 @@ public function entityBaseFieldInfo(EntityTypeInterface $entity_type) { ->setSetting('target_type', 'moderation_state') ->setDisplayOptions('view', [ 'label' => 'hidden', - 'type' => 'hidden', + 'region' => 'hidden', 'weight' => -5, ]) ->setDisplayOptions('form', [ diff --git a/core/modules/field/src/Entity/FieldConfig.php b/core/modules/field/src/Entity/FieldConfig.php index d7ccc30..f5e8ab3 100644 --- a/core/modules/field/src/Entity/FieldConfig.php +++ b/core/modules/field/src/Entity/FieldConfig.php @@ -306,7 +306,7 @@ public function isDisplayConfigurable($context) { */ public function getDisplayOptions($display_context) { // Hide configurable fields by default. - return array('type' => 'hidden'); + return array('region' => 'hidden'); } /** diff --git a/core/modules/field/src/Tests/EntityReference/EntityReferenceAdminTest.php b/core/modules/field/src/Tests/EntityReference/EntityReferenceAdminTest.php index 0324c4a..3803029 100644 --- a/core/modules/field/src/Tests/EntityReference/EntityReferenceAdminTest.php +++ b/core/modules/field/src/Tests/EntityReference/EntityReferenceAdminTest.php @@ -375,7 +375,6 @@ public function testAvailableFormatters() { 'entity_reference_entity_id', 'entity_reference_rss_category', 'entity_reference_entity_view', - 'hidden', )); // Test if User Reference Field has the correct formatters. @@ -386,7 +385,6 @@ public function testAvailableFormatters() { 'entity_reference_entity_id', 'entity_reference_entity_view', 'entity_reference_label', - 'hidden', )); // Test if Node Entity Reference Field has the correct formatters. @@ -395,7 +393,6 @@ public function testAvailableFormatters() { 'entity_reference_label', 'entity_reference_entity_id', 'entity_reference_entity_view', - 'hidden', )); // Test if Date Format Reference Field has the correct formatters. @@ -404,7 +401,6 @@ public function testAvailableFormatters() { $this->assertFieldSelectOptions('fields[field_' . $date_format_field_name . '][type]', array( 'entity_reference_label', 'entity_reference_entity_id', - 'hidden', )); } diff --git a/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldFormatterSettingsTest.php b/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldFormatterSettingsTest.php index 4c9cbf1..2ee3c77 100644 --- a/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldFormatterSettingsTest.php +++ b/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldFormatterSettingsTest.php @@ -32,6 +32,7 @@ public function testEntityDisplaySettings() { 'type' => 'text_trimmed', 'settings' => array('trim_length' => 600), 'third_party_settings' => array(), + 'region' => 'content', ); // Can we load any entity display. diff --git a/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldWidgetSettingsTest.php b/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldWidgetSettingsTest.php index a16040f..e7bb636 100644 --- a/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldWidgetSettingsTest.php +++ b/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldWidgetSettingsTest.php @@ -33,6 +33,7 @@ public function testWidgetSettings() { $expected = array('weight' => 1, 'type' => 'text_textfield'); $expected['settings'] = array('size' => 60, 'placeholder' => ''); $expected['third_party_settings'] = array(); + $expected['region'] = 'content'; $this->assertIdentical($expected, $component, 'Text field settings are correct.'); // Integer field. 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..afe2da4 --- /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..67f2ba0 --- /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_plugin 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..1a4806a --- /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 for content forms and displays.') . '

'; + $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
. + * + * @ingroup themeable + */ +#} +{% +set classes = [ +'field-layout--onecol', +] +%} +{% if content %} + +
+ {{ 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..47c6de2 --- /dev/null +++ b/core/modules/field_layout/src/FieldLayoutBuilder.php @@ -0,0 +1,123 @@ +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); + } + } + + /** + * 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..3e91740 --- /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][type]', '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][type]', '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..9fa4ada --- /dev/null +++ b/core/modules/field_layout/tests/src/Unit/FieldLayoutBuilderTest.php @@ -0,0 +1,281 @@ +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', + ], + ], + ], + ]; + $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', + ], + ], + ], + ]; + $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); + } + +}