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/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/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/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/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/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/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..2dd8a83 --- /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.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..9f79868 --- /dev/null +++ b/core/modules/field_layout/field_layout.install @@ -0,0 +1,27 @@ +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..331123d --- /dev/null +++ b/core/modules/field_layout/field_layout.module @@ -0,0 +1,95 @@ +' . 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_TYPE_presave() for entity_form_display entities. + */ +function field_layout_entity_form_display_presave(EntityInterface $entity) { + _field_layout_entity_display_presave($entity); +} + +/** + * Implements hook_ENTITY_TYPE_presave() for entity_view_display entities. + */ +function field_layout_entity_view_display_presave(EntityInterface $entity) { + _field_layout_entity_display_presave($entity); +} + +/** + * Ensures there is a layout set on a display entity. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity object. + */ +function _field_layout_entity_display_presave(EntityInterface $entity) { + if ($entity instanceof EntityDisplayWithLayoutInterface && !$entity->getLayoutId()) { + $entity->setLayout('onecol'); + } +} + +/** + * 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..83a907e --- /dev/null +++ b/core/modules/field_layout/src/Display/EntityDisplayWithLayoutInterface.php @@ -0,0 +1,48 @@ +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; + } + +} 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..d34f243 --- /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'] = $this->layoutPluginManager->createInstance($display->getLayoutId(), $display->getLayoutSettings())->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..b087ade --- /dev/null +++ b/core/modules/field_layout/src/Form/FieldLayoutEntityDisplayFormTrait.php @@ -0,0 +1,169 @@ +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_options = []; + foreach ($this->layoutPluginManager->getGroupedDefinitions() as $category => $layout_definitions) { + foreach ($layout_definitions as $name => $layout_definition) { + $layout_options[$category][$name] = $layout_definition->getLabel(); + } + } + $layout_id = $form_state->getValue('field_layout') ?: ($this->getEntity()->getLayoutId() ?: 'onecol'); + $form['field_layouts']['field_layout'] = [ + '#type' => 'select', + '#title' => $this->t('Select a layout'), + '#options' => $layout_options, + '#default_value' => $layout_id, + '#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, + ]; + + $layout_plugin = $this->layoutPluginManager->createInstance($layout_id, $this->getEntity()->getLayoutSettings()); + $form_state->set('layout_plugin', $layout_plugin); + 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; + } + + /** + * 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->setRebuild(); + } + + /** + * Overrides \Drupal\field_ui\Form\EntityDisplayFormBase::validateForm(). + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + parent::validateForm($form, $form_state); + + $layout_plugin = $form_state->get('layout_plugin'); + 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 = $form_state->get('layout_plugin'); + 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..a5d12d0 --- /dev/null +++ b/core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.info.yml @@ -0,0 +1,9 @@ +name: 'Field Layout test' +type: module +description: 'Support module for Field Layout tests.' +core: 8.x +package: Testing +version: VERSION +dependencies: + - entity_test + - layout_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..628eefb --- /dev/null +++ b/core/modules/field_layout/tests/src/FunctionalJavascript/FieldLayoutTest.php @@ -0,0 +1,254 @@ + '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'); + $this->getSession()->getPage()->selectFieldOption('field_layout', 'layout_test_plugin'); + $this->assertSession()->assertWaitOnAjaxRequest(); + // 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..bc48a14 --- /dev/null +++ b/core/modules/field_layout/tests/src/Unit/FieldLayoutBuilderTest.php @@ -0,0 +1,261 @@ +layoutPluginManager = $this->prophesize(LayoutPluginManagerInterface::class); + $this->layoutPluginManager->getDefinition('unknown', FALSE)->willReturn(NULL); + + $twocol_definition = new LayoutDefinition([ + 'library' => 'field_layout/drupal.field_layout.twocol', + 'theme' => 'field_layout__twocol', + 'regions' => [ + 'left' => [ + 'label' => 'Left', + ], + 'right' => [ + 'label' => 'Right', + ], + ], + ]); + $layout_plugin = new LayoutDefault([], 'twocol', $twocol_definition); + $this->layoutPluginManager->createInstance('twocol', [])->willReturn($layout_plugin); + $this->layoutPluginManager->getDefinition('twocol', FALSE)->willReturn($twocol_definition); + + $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->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' => [ + 'left' => [], + 'right' => [ + 'test1' => [ + '#markup' => 'Test1', + ], + ], + '#settings' => [], + '#theme' => 'field_layout__twocol', + '#attached' => [ + 'library' => [ + 'field_layout/drupal.field_layout.twocol', + ], + ], + ], + ]; + $this->fieldLayoutBuilder->build($build, $display->reveal(), $display_context); + $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->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' => [ + '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' => [], + '#theme' => 'field_layout__twocol', + '#attached' => [ + 'library' => [ + 'field_layout/drupal.field_layout.twocol', + ], + ], + ], + ]; + $this->fieldLayoutBuilder->build($build, $display->reveal(), $display_context); + $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->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..6933136 100644 --- a/core/modules/field_ui/field_ui.js +++ b/core/modules/field_ui/field_ui.js @@ -128,9 +128,13 @@ var refreshRows = {}; refreshRows[rowHandler.name] = $trigger.get(0); - // Handle region change. + // Handle region or type change. var region = rowHandler.getRegion(); - if (region !== rowHandler.region) { + var typeRegion = rowHandler.getType(); + if (region !== rowHandler.region || typeRegion !== rowHandler.region) { + if (region === rowHandler.region) { + region = typeRegion; + } // Remove parenting. $row.find('select.js-field-parent').val(''); // Let the row handler deal with the region change. @@ -270,6 +274,10 @@ else if ($this.is('.region-empty')) { 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,6 +290,16 @@ else if ($this.is('.region-empty')) { * Either 'hidden' or 'content'. */ getRegion: function () { + return this.$regionSelect.val(); + }, + + /** + * Returns the region corresponding to the current form values of the row. + * + * @returns {string} + * Either 'hidden' or 'content'. + */ + getType: function () { return (this.$pluginSelect.val() === 'hidden') ? 'hidden' : 'content'; }, @@ -305,14 +323,17 @@ else if ($this.is('.region-empty')) { * {@link Drupal.fieldUIOverview.AJAXRefreshRows}. */ regionChange: function (region) { + // Replace dashes with underscores. + region = region.replace(/-/g, '_'); + + // Set the region of the select list. + this.$regionSelect.val(region); // 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 (region !== 'hidden') { if (currentValue === 'hidden') { // Restore the formatter back to the default formatter. Pseudo-fields // do not have default formatters, we just return to 'visible' for diff --git a/core/modules/field_ui/src/Form/EntityDisplayFormBase.php b/core/modules/field_ui/src/Form/EntityDisplayFormBase.php index 586f1ef..ce69fb7 100644 --- a/core/modules/field_ui/src/Form/EntityDisplayFormBase.php +++ b/core/modules/field_ui/src/Form/EntityDisplayFormBase.php @@ -4,6 +4,7 @@ use Drupal\Component\Plugin\Factory\DefaultFactory; use Drupal\Component\Plugin\PluginManagerBase; +use Drupal\Core\Entity\Display\EntityDisplayInterface; use Drupal\Core\Entity\EntityForm; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityWithPluginCollectionInterface; @@ -172,6 +173,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 +317,15 @@ 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(), + '#empty_value' => 'hidden', + '#default_value' => isset($display_options['region']) ? $display_options['region'] : 'hidden', + '#attributes' => array('class' => array('field-region')), + ), ); $field_row['plugin'] = array( @@ -474,6 +491,15 @@ 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(), + '#empty_value' => 'hidden', + '#default_value' => $display_options ? $display_options['region'] : 'hidden', + '#attributes' => array('class' => array('field-region')), + ), 'plugin' => array( 'type' => array( '#type' => 'select', @@ -550,7 +576,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 +593,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 +604,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'], )); } } @@ -813,7 +841,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'; } } 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..05178ba 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; @@ -98,8 +99,28 @@ function testFormatterUI() { ); $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 +168,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 +183,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'); @@ -230,7 +258,11 @@ public function testWidgetUI() { $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); @@ -284,6 +316,14 @@ public function testWidgetUI() { // 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')); + + // 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 +361,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 +376,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."); 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..7cde00c --- /dev/null +++ b/core/modules/field_ui/tests/src/FunctionalJavascript/EntityDisplayTest.php @@ -0,0 +1,95 @@ + '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->assertFalse($this->assertSession()->optionExists('fields[field_test_text][type]', 'hidden')->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->assertTrue($this->assertSession()->optionExists('fields[field_test_text][type]', '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->assertTrue($this->assertSession()->optionExists('fields[field_test_text][type]', '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->assertTrue($this->assertSession()->optionExists('fields[field_test_text][type]', '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->assertFalse($this->assertSession()->optionExists('fields[field_test_text][type]', 'hidden')->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->assertFalse($this->assertSession()->optionExists('fields[field_test_text][type]', 'hidden')->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..45fcee2 --- /dev/null +++ b/core/modules/layout_plugin/config/schema/layout_plugin.schema.yml @@ -0,0 +1,5 @@ +layout.settings: + type: mapping + +layout.settings.*: + type: layout.settings diff --git a/core/modules/layout_plugin/layout_plugin.info.yml b/core/modules/layout_plugin/layout_plugin.info.yml new file mode 100644 index 0000000..7058dca --- /dev/null +++ b/core/modules/layout_plugin/layout_plugin.info.yml @@ -0,0 +1,6 @@ +name: 'Layout Plugin' +type: module +description: 'Provides a way for modules or themes to register layouts.' +package: Core (Experimental) +version: VERSION +core: 8.x diff --git a/core/modules/layout_plugin/layout_plugin.module b/core/modules/layout_plugin/layout_plugin.module new file mode 100644 index 0000000..f35c1dd --- /dev/null +++ b/core/modules/layout_plugin/layout_plugin.module @@ -0,0 +1,47 @@ +' . 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(); +} + +/** + * Implements hook_theme_registry_alter(). + */ +function layout_plugin_theme_registry_alter(&$theme_registry) { + \Drupal::service('plugin.manager.layout_plugin')->alterThemeImplementations($theme_registry); +} + +/** + * Prepares variables for layout templates. + * + * @param array &$variables + * An associative array containing: + * - element: An associative array containing the properties of the element. + * Properties used: #settings, #layout. + */ +function _layout_plugin_preprocess_layout(&$variables) { + $variables['settings'] = isset($variables['content']['#settings']) ? $variables['content']['#settings'] : []; +} 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..13595a2 --- /dev/null +++ b/core/modules/layout_plugin/src/Annotation/Layout.php @@ -0,0 +1,129 @@ +definition); + } + +} diff --git a/core/modules/layout_plugin/src/LayoutPluginManager.php b/core/modules/layout_plugin/src/LayoutPluginManager.php new file mode 100644 index 0000000..df2b7e1 --- /dev/null +++ b/core/modules/layout_plugin/src/LayoutPluginManager.php @@ -0,0 +1,218 @@ +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 ContainerDerivativeDiscoveryDecorator($discovery); + $this->discovery = $discovery; + } + return $this->discovery; + } + + /** + * {@inheritdoc} + */ + public function processDefinition(&$definition, $plugin_id) { + parent::processDefinition($definition, $plugin_id); + + if (!$definition instanceof LayoutDefinitionInterface) { + throw new InvalidPluginDefinitionException($plugin_id, sprintf('The "%s" layout definition must implement interface %s', $plugin_id, LayoutDefinitionInterface::class)); + } + + // 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); + + // If 'template' is set, then we'll derive 'template_path' and 'theme'. + $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->setTheme(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\LayoutDefinitionInterface[] $definitions */ + $definitions = $this->getDefinitions(); + foreach ($definitions as $definition) { + if ($definition->hasThemeImplementation()) { + $hooks[$definition->getTheme()] = [ + 'render element' => 'content', + 'template' => $definition->getTemplate(), + 'path' => $definition->getTemplatePath(), + ]; + } + } + return $hooks; + } + + /** + * {@inheritdoc} + */ + public function alterThemeImplementations(array &$theme_registry) { + /** @var \Drupal\layout_plugin\Plugin\Layout\LayoutDefinitionInterface[] $definitions */ + $definitions = $this->getDefinitions(); + + // Find all the theme hooks which are for automatically registered templates + // (we ignore manually set theme hooks because we don't know how they were + // registered). + $layout_theme_hooks = []; + foreach ($definitions as $definition) { + if ($definition->hasThemeImplementation() && isset($theme_registry[$definition->getTheme()])) { + $layout_theme_hooks[] = $definition->getTheme(); + } + } + + // Go through the theme registry looking for our theme hooks and any + // suggestions based on them. + foreach ($theme_registry as $theme_hook => &$info) { + if (in_array($theme_hook, $layout_theme_hooks) || (!empty($info['base hook']) && in_array($info['base hook'], $layout_theme_hooks))) { + // If 'template_preprocess' is included, we want to put our preprocess + // after to not mess up the expectation that 'template_process' always + // runs first. + if (($index = array_search('template_preprocess', $info['preprocess functions'])) !== FALSE) { + $index++; + } + else { + // Otherwise, put our preprocess function first. + $index = 0; + } + + array_splice($info['preprocess functions'], $index, 0, '_layout_plugin_preprocess_layout'); + } + } + } + + /** + * {@inheritdoc} + */ + public function getCategories() { + // Fetch all categories from definitions and remove duplicates. + $categories = array_unique(array_values(array_map(function (LayoutDefinitionInterface $definition) { + return $definition->getCategory(); + }, $this->getDefinitions()))); + natcasesort($categories); + return $categories; + } + + /** + * {@inheritdoc} + * + * @return \Drupal\layout_plugin\Plugin\Layout\LayoutDefinitionInterface[] + */ + 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 (LayoutDefinitionInterface $a, LayoutDefinitionInterface $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\LayoutDefinitionInterface[][] + */ + 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; + } + +} diff --git a/core/modules/layout_plugin/src/LayoutPluginManagerInterface.php b/core/modules/layout_plugin/src/LayoutPluginManagerInterface.php new file mode 100644 index 0000000..cebc0c3 --- /dev/null +++ b/core/modules/layout_plugin/src/LayoutPluginManagerInterface.php @@ -0,0 +1,61 @@ +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..6549108 --- /dev/null +++ b/core/modules/layout_plugin/src/Plugin/Layout/LayoutBase.php @@ -0,0 +1,78 @@ +setConfiguration($configuration); + } + + /** + * {@inheritdoc} + */ + public function build(array $regions) { + $build = array_intersect_key($regions, $this->pluginDefinition->getRegions()); + $build['#settings'] = $this->getConfiguration(); + $build['#theme'] = $this->pluginDefinition->getTheme(); + 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\LayoutDefinitionInterface + */ + 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->{$property} = $value; + } + } + + /** + * {@inheritdoc} + */ + public function id() { + return $this->id; + } + + /** + * {@inheritdoc} + */ + public function getClass() { + return $this->class; + } + + /** + * {@inheritdoc} + */ + 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; + } + + /** + * {@inheritdoc} + */ + public function getLabel() { + return $this->label; + } + + /** + * {@inheritdoc} + */ + public function setLabel($label) { + $this->label = $label; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getCategory() { + return $this->category; + } + + /** + * {@inheritdoc} + */ + public function setCategory($category) { + $this->category = $category; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getTemplate() { + return $this->template; + } + + /** + * {@inheritdoc} + */ + public function setTemplate($template) { + $this->template = $template; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getTemplatePath() { + return $this->templatePath; + } + + /** + * {@inheritdoc} + */ + public function setTemplatePath($template_path) { + $this->templatePath = $template_path; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getTheme() { + return $this->theme; + } + + /** + * {@inheritdoc} + */ + public function hasThemeImplementation() { + return $this->getTemplate() && $this->getTheme(); + } + + /** + * {@inheritdoc} + */ + public function setTheme($theme) { + $this->theme = $theme; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getPath() { + return $this->path; + } + + /** + * {@inheritdoc} + */ + public function setPath($path) { + $this->path = $path; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getLibrary() { + return $this->library; + } + + /** + * {@inheritdoc} + */ + public function setLibrary($library) { + $this->library = $library; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getRegions() { + return $this->regions; + } + + /** + * {@inheritdoc} + */ + public function setRegions(array $regions) { + $this->regions = $regions; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getRegionNames() { + return array_keys($this->getRegions()); + } + + /** + * {@inheritdoc} + */ + public function getDefaultRegion() { + return $this->default_region; + } + + /** + * {@inheritdoc} + */ + public function setDefaultRegion($default_region) { + $this->default_region = $default_region; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getProvider() { + return $this->provider; + } + +} diff --git a/core/modules/layout_plugin/src/Plugin/Layout/LayoutDefinitionInterface.php b/core/modules/layout_plugin/src/Plugin/Layout/LayoutDefinitionInterface.php new file mode 100644 index 0000000..64b0364 --- /dev/null +++ b/core/modules/layout_plugin/src/Plugin/Layout/LayoutDefinitionInterface.php @@ -0,0 +1,223 @@ + '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..55d3ac2 --- /dev/null +++ b/core/modules/layout_plugin/tests/src/Unit/LayoutPluginManagerTest.php @@ -0,0 +1,406 @@ +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(LayoutDefinitionInterface::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->getTheme()); + $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->getTheme()); + $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->getTheme()); + $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 implement interface'); + $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' => [ + 'render element' => 'content', + 'template' => 'twocol', + 'path' => "$theme_a_path/templates", + ], + 'plugin_provided_layout' => [ + 'render element' => 'content', + 'template' => 'plugin-provided-layout', + 'path' => "$core_path/templates", + ], + ]; + $theme_implementations = $this->layoutPluginManager->getThemeImplementations(); + $this->assertSame($expected, $theme_implementations); + return $theme_implementations; + } + + /** + * @covers ::alterThemeImplementations + * @depends testGetThemeImplementations + */ + public function testAlterThemeImplementations(array $theme_implementations) { + $expected = $theme_implementations; + + $theme_implementations['twocol']['preprocess functions'][] = 'other_preprocess_function'; + $theme_implementations['plugin_provided_layout']['preprocess functions'][] = 'template_preprocess'; + $theme_implementations['plugin_provided_layout']['preprocess functions'][] = 'other_preprocess_function'; + + $expected['twocol']['preprocess functions'][] = '_layout_plugin_preprocess_layout'; + $expected['twocol']['preprocess functions'][] = 'other_preprocess_function'; + $expected['plugin_provided_layout']['preprocess functions'][] = 'template_preprocess'; + $expected['plugin_provided_layout']['preprocess functions'][] = '_layout_plugin_preprocess_layout'; + $expected['plugin_provided_layout']['preprocess functions'][] = 'other_preprocess_function'; + + $this->layoutPluginManager->alterThemeImplementations($theme_implementations); + $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(LayoutDefinitionInterface::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(LayoutDefinitionInterface::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: 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: + 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 (!empty($base_plugin_definition->array_based)) { + $this->derivatives['array_based'] = []; + } + if (!empty($base_plugin_definition->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..9f9c9c2 100644 --- a/core/modules/node/src/Tests/NodeTypeInitialLanguageTest.php +++ b/core/modules/node/src/Tests/NodeTypeInitialLanguageTest.php @@ -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..fe3167f 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/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..86efc77 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityDisplayFormBaseTest.php @@ -0,0 +1,283 @@ +prophesize(EntityDisplayInterface::class); + $entity->getPluginCollections()->willReturn([]); + + // A field with no initial values, with mismatched submitted values, type is + // hidden. + $entity->getComponent('new_field_mismatch_type_hidden')->willReturn([]); + $field_values['new_field_mismatch_type_hidden'] = [ + 'weight' => 0, + 'type' => 'hidden', + 'region' => 'content', + ]; + $entity + ->setComponent('new_field_mismatch_type_hidden', [ + 'weight' => 0, + 'type' => 'hidden', + 'region' => 'content', + ]) + ->will(function($args) { + // On subsequent calls, getComponent() will return the newly set values. + $this->getComponent($args[0])->willReturn($args[1]); + }) + ->shouldBeCalled(); + + // A field with no initial values, with mismatched submitted values, type is + // visible. + $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 hidden field, with identical submitted values. + $entity->getComponent('field_hidden_no_changes') + ->willReturn([ + 'weight' => 0, + 'type' => 'hidden', + 'region' => 'hidden', + ]); + $field_values['field_hidden_no_changes'] = [ + 'weight' => 0, + 'type' => 'hidden', + 'region' => 'hidden', + ]; + $entity->removeComponent('field_hidden_no_changes') + ->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 hidden field, with a submitted type change. + $entity->getComponent('field_start_hidden_change_type') + ->willReturn([ + 'weight' => 0, + 'type' => 'hidden', + 'region' => 'hidden', + ]); + $field_values['field_start_hidden_change_type'] = [ + 'weight' => 0, + 'type' => 'textfield', + 'region' => 'hidden', + ]; + $entity->removeComponent('field_start_hidden_change_type') + ->will(function ($args) { + // On subsequent calls, getComponent() will return an empty array. + $this->getComponent($args[0])->willReturn([]); + }) + ->shouldBeCalled(); + + // An initially hidden field, with a submitted region change. + $entity->getComponent('field_start_hidden_change_region') + ->willReturn([ + 'weight' => 0, + 'type' => 'hidden', + 'region' => 'hidden', + ]); + $field_values['field_start_hidden_change_region'] = [ + 'weight' => 0, + 'type' => 'hidden', + 'region' => 'content', + ]; + $entity + ->setComponent('field_start_hidden_change_region', [ + 'weight' => 0, + 'type' => 'hidden', + 'region' => 'content', + ]) + ->will(function($args) { + // On subsequent calls, getComponent() will return the newly set values. + $this->getComponent($args[0])->willReturn($args[1]); + }) + ->shouldBeCalled(); + + // An initially hidden field, with a submitted region and type change. + $entity->getComponent('field_start_hidden_change_both') + ->willReturn([ + 'weight' => 0, + 'type' => 'hidden', + 'region' => 'hidden', + ]); + $field_values['field_start_hidden_change_both'] = [ + 'weight' => 0, + 'type' => 'textfield', + 'region' => 'content', + ]; + $entity + ->setComponent('field_start_hidden_change_both', [ + '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]); + }) + ->shouldBeCalled(); + + // An initially visible field, with a submitted type change. + $entity->getComponent('field_start_visible_change_type') + ->willReturn([ + 'weight' => 0, + 'type' => 'textfield', + 'region' => 'content', + ]); + $field_values['field_start_visible_change_type'] = [ + 'weight' => 0, + 'type' => 'hidden', + 'region' => 'content', + ]; + $entity + ->setComponent('field_start_visible_change_type', [ + 'weight' => 0, + 'type' => 'hidden', + 'region' => 'content', + ]) + ->will(function($args) { + // On subsequent calls, getComponent() will return the newly set values. + $this->getComponent($args[0])->willReturn($args[1]); + }) + ->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(); + + // An initially visible field, with a submitted region and type change. + $entity->getComponent('field_start_visible_change_both') + ->willReturn([ + 'weight' => 0, + 'type' => 'textfield', + 'region' => 'content', + ]); + $field_values['field_start_visible_change_both'] = [ + 'weight' => 0, + 'type' => 'hidden', + 'region' => 'hidden', + ]; + $entity->removeComponent('field_start_visible_change_both') + ->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->setEntity($entity->reveal()); + + $form = [ + '#fields' => array_keys($field_values), + '#extra' => [], + ]; + $form_state = new FormState(); + $form_state->setValues(['fields' => $field_values]); + + $form_object->buildEntity($form, $form_state); + + // 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); + } + +}