diff --git a/core/config/schema/core.entity.schema.yml b/core/config/schema/core.entity.schema.yml index df2a2fd..7f24836 100644 --- a/core/config/schema/core.entity.schema.yml +++ b/core/config/schema/core.entity.schema.yml @@ -362,3 +362,19 @@ field.formatter.settings.entity_reference_label: type: boolean label: 'Link label to the referenced entity' +field.formatter.settings.entity_reference_inline_settings: + type: mapping + label: 'Entity reference inline formatter display format settings' + mapping: + field_name: + label: 'Machine name of the field' + type: string + type: + label: 'Machine name of the formatter' + type: string + settings: + label: 'Formatter settings' + type: field.formatter.settings.[type] + label: + label: 'Target field label visibility setting' + type: string diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/EntityReferenceInlineSettingsFormatter.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/EntityReferenceInlineSettingsFormatter.php new file mode 100644 index 0000000..88db08c --- /dev/null +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/EntityReferenceInlineSettingsFormatter.php @@ -0,0 +1,468 @@ +get('entity_field.manager'), + $container->get('plugin.manager.field.formatter'), + $container->get('entity_type.manager'), + $container->get('entity_type.bundle.info') + ); + } + + /** + * Constructs a EntityReferenceInlineSettingsFormatter object. + * + * @param string $plugin_id + * The plugin_id for the formatter. + * @param mixed $plugin_definition + * The plugin implementation definition. + * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition + * The definition of the field to which the formatter is associated. + * @param array $settings + * The formatter settings. + * @param string $label + * The formatter label display setting. + * @param string $view_mode + * The view mode. + * @param array $third_party_settings + * Any third party settings. + * @param \Drupal\Core\Entity\EntityFieldManager $entity_field_manager + * The entity field manager. + * @param \Drupal\Core\Field\FormatterPluginManager $formatter_plugin_manager + * The formatter plugin manager. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info + * The entity type bundle info. + */ + public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, EntityFieldManager $entity_field_manager, FormatterPluginManager $formatter_plugin_manager, EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info) { + parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings); + $this->entityFieldManager = $entity_field_manager; + $this->formatterPluginManager = $formatter_plugin_manager; + $this->entityTypeManager = $entity_type_manager; + $this->entityTypeBundleInfo = $entity_type_bundle_info; + } + + /** + * {@inheritdoc} + */ + public static function defaultSettings() { + return [ + 'field_name' => '', + 'type' => '', + 'settings' => [], + 'label' => 'above', + ]; + } + + /** + * Gets field definition for given field storage definition. + * + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $field_storage_definition + * The field storage definition. + * + * @return \Drupal\Core\Field\BaseFieldDefinition + * The field definition. + */ + protected function getFieldDefinition(FieldStorageDefinitionInterface $field_storage_definition) { + return BaseFieldDefinition::createFromFieldStorageDefinition($field_storage_definition); + } + + /** + * {@inheritdoc} + */ + public function viewElements(FieldItemListInterface $items, $langcode) { + /** @var \Drupal\Core\Entity\FieldableEntityInterface $entity */ + $entities = $this->getEntitiesToView($items, $langcode); + + $build = []; + foreach ($entities as $delta => $entity) { + $build[$delta] = $this->getViewDisplay($entity->bundle())->build($entity); + } + return $build; + } + + /** + * Gets a list of supported fields. + * + * @return array + * List of fields that are supported keyed by field machine name. + */ + protected function getAvailableFieldNames() { + $entity_type_id = $this->fieldDefinition->getSetting('target_type'); + $entity_type = $this->entityTypeManager->getDefinition($entity_type_id); + // We always show the entity label as an option to be selected. + $label_key = $entity_type->getKey('label'); + $field_names = [$label_key => $this->entityFieldManager->getBaseFieldDefinitions($entity_type_id)[$label_key]->getLabel()]; + $target_bundles = $this->fieldDefinition->getSetting('handler_settings')['target_bundles'] === NULL ? array_keys($this->entityTypeBundleInfo->getBundleInfo($entity_type_id)) : $this->fieldDefinition->getSetting('handler_settings')['target_bundles']; + foreach ($target_bundles as $value) { + $bundle_field_names = array_map( + function (FieldDefinitionInterface $field_definition) { + if ($field_definition->isDisplayConfigurable('view')) { + return $field_definition->getLabel(); + } + }, + $this->entityFieldManager->getFieldDefinitions($entity_type_id, $value) + ); + $field_names = array_merge($field_names, array_filter($bundle_field_names)); + } + return $field_names; + } + + /** + * {@inheritdoc} + */ + public static function isApplicable(FieldDefinitionInterface $field_definition) { + $entity_type = \Drupal::entityTypeManager() + ->getDefinition($field_definition->getTargetEntityTypeId()); + return $entity_type->isSubclassOf(FieldableEntityInterface::class); + } + + /** + * Get all available formatters for a field storage definition. + * + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $field_storage_definition + * The field storage definition. + * + * @return string[] + * The field formatter labels keys by plugin ID. + */ + protected function getAvailableFormatterOptions(FieldStorageDefinitionInterface $field_storage_definition) { + $field_definition = $this->getFieldDefinition($field_storage_definition); + $formatters = $this->formatterPluginManager->getOptions($field_storage_definition->getType()); + $formatter_instances = array_map(function($formatter_id) use ($field_definition) { + $configuration = [ + 'field_definition' => $field_definition, + 'settings' => [], + 'label' => '', + 'view_mode' => '', + 'third_party_settings' => [], + ]; + return $this->formatterPluginManager->createInstance($formatter_id, $configuration); + }, array_combine(array_keys($formatters), array_keys($formatters))); + $filtered_formatter_instances = array_filter($formatter_instances, function (FormatterInterface $formatter) use ($field_definition) { + return $formatter->isApplicable($field_definition); + }); + $options = array_map(function (FormatterInterface $formatter) { + return $formatter->getPluginDefinition()['label']; + }, $filtered_formatter_instances); + return $options; + } + + /** + * Ajax callback for field name change. + * + * @param array $form + * The form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * + * @return array + * The replaced form substructure. + */ + public static function onFieldNameChange(array $form, FormStateInterface $form_state) { + return $form['fields'][$form_state->getStorage()['plugin_settings_edit']]['plugin']['settings_edit_form']['settings']; + } + + /** + * Ajax callback for formatter type change. + * + * @param array $form + * The form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * + * @return array + * The replaced form substructure. + */ + public static function onFormatterTypeChange(array $form, FormStateInterface $form_state) { + return $form['fields'][$form_state->getStorage()['plugin_settings_edit']]['plugin']['settings_edit_form']['settings']['settings']; + } + + /** + * Ajax callback for "show basefields" setting change. + * + * @param array $form + * The form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * + * @return array + * The replaced form substructure. + */ + public static function onShowBasefieldsChange(array $form, FormStateInterface $form_state) { + return $form['fields'][$form_state->getStorage()['plugin_settings_edit']]['plugin']['settings_edit_form']['settings']; + } + + /** + * Rebuilds the form on select submit. + * + * @param array $form + * The form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + */ + public static function rebuildSubmit(array $form, FormStateInterface $form_state) { + $form_state->setRebuild(TRUE); + } + + /** + * {@inheritdoc} + */ + public function settingsForm(array $form, FormStateInterface $form_state) { + $form = parent::settingsForm($form, $form_state); + $target_entity_type_id = $this->fieldDefinition->getSetting('target_type'); + $field_name_options = $this->getAvailableFieldNames(); + $field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions($target_entity_type_id); + // Field on the target entity this formatter is currently displaying. + $selected_field_name = $this->getSettingFromFormState($form_state, 'field_name'); + if (!$selected_field_name) { + // The first field is used as default in case of no value set. + $selected_field_name = key($field_name_options); + } + $field_storage = $field_storage_definitions[$selected_field_name]; + + $form['#prefix'] = '
'; + $form['#suffix'] = '
'; + + $form['field_name'] = [ + '#type' => 'select', + '#title' => $this->t('Field name'), + '#default_value' => $selected_field_name, + '#options' => $field_name_options, + '#ajax' => [ + 'callback' => [static::class, 'onFieldNameChange'], + 'wrapper' => 'field-formatter-ajax', + 'method' => 'replace', + ], + '#submit' => [[static::class, 'rebuildSubmit']], + '#executes_submit_callback' => TRUE, + ]; + + $form['label'] = [ + '#type' => 'select', + '#title' => $this->t('Label'), + '#options' => [ + 'above' => $this->t('Above'), + 'inline' => $this->t('Inline'), + 'hidden' => '- ' . $this->t('Hidden') . ' -', + 'visually_hidden' => '- ' . $this->t('Visually Hidden') . ' -', + ], + '#default_value' => $this->getSettingFromFormState($form_state, 'label'), + ]; + + if ($selected_field_name && !empty($field_storage)) { + $formatter_options = $this->getAvailableFormatterOptions($field_storage); + $formatter_type = $this->getSettingFromFormState($form_state, 'type'); + $settings = $this->getSettingFromFormState($form_state, 'settings'); + if (!isset($formatter_options[$formatter_type])) { + $formatter_type = key($formatter_options); + $settings = []; + } + + $form['type'] = [ + '#type' => 'select', + '#title' => $this->t('Formatter'), + '#options' => $formatter_options, + '#default_value' => $formatter_type, + '#ajax' => [ + 'callback' => [static::class, 'onFormatterTypeChange'], + 'wrapper' => 'field-formatter-settings-ajax', + 'method' => 'replace', + ], + '#submit' => [[static::class, 'rebuildSubmit']], + '#executes_submit_callback' => TRUE, + ]; + + $options = [ + 'field_definition' => $this->getFieldDefinition($field_storage), + 'configuration' => [ + 'type' => $formatter_type, + 'settings' => $settings, + 'label' => '', + 'weight' => 0, + ], + 'view_mode' => '_custom', + ]; + + // Get the formatter settings form. + $settings_form = ['#value' => []]; + if ($formatter = $this->formatterPluginManager->getInstance($options)) { + $settings_form = $formatter->settingsForm([], $form_state); + } + $form['settings'] = $settings_form; + $form['settings']['#prefix'] = '
'; + $form['settings']['#suffix'] = '
'; + } + + return $form; + } + + /** + * Gets the entity view display for a bundle. + * + * @param string $bundle_id + * The bundle ID. + * + * @return \Drupal\Core\Entity\Display\EntityViewDisplayInterface + * Entity view display. + */ + protected function getViewDisplay($bundle_id) { + if (!isset($this->viewDisplay[$bundle_id])) { + $this->viewDisplay[$bundle_id] = $this->createFormatterEntityViewDisplay($bundle_id); + } + return $this->viewDisplay[$bundle_id]; + } + + /** + * Creates the entity view display for a bundle. + * + * @param string $bundle_id + * The bundle ID. + * + * @return \Drupal\Core\Entity\Display\EntityViewDisplayInterface + * Entity view display. + */ + protected function createFormatterEntityViewDisplay($bundle_id) { + $display = EntityViewDisplay::create([ + 'targetEntityType' => $this->fieldDefinition->getSetting('target_type'), + 'bundle' => $bundle_id, + 'status' => TRUE, + ]); + $display->setComponent($this->getSetting('field_name'), [ + 'type' => $this->getSetting('type'), + 'settings' => $this->getSetting('settings'), + 'label' => $this->getSetting('label'), + ]); + return $display; + } + + /** + * {@inheritdoc} + */ + public function settingsSummary() { + $summary = parent::settingsSummary(); + $field_storage_definitions = NULL; + + if ($field_name = $this->getSetting('field_name')) { + $summary[] = $this->t('Field %field_name displayed.', ['%field_name' => $field_name]); + } + else { + $field_name_options = $this->getAvailableFieldNames(); + $target_entity_type_id = $this->fieldDefinition->getSetting('target_type'); + $field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions($target_entity_type_id); + $field_name = key($field_name_options); + $summary[] = $this->t('The field %field_name will be used by default.', ['%field_name' => $field_name]); + } + + if ($type = $this->getSetting('type')) { + $summary[] = $this->t('Formatter %type used.', ['%type' => $type]); + } + elseif ($field_storage_definitions) { + $field_storage = $field_storage_definitions[$field_name]; + $formatter_options = $this->getAvailableFormatterOptions($field_storage); + $formatter_type = key($formatter_options); + $summary[] = $this->t('The %type formatter will be used by default.', ['%type' => $formatter_type]); + } + else { + $summary[] = $this->t('Formatter not configured. The first formatter will be used.'); + } + + return $summary; + } + + /** + * Wrapper around ::getSetting() to carry over values from the form state. + * + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * @param string $setting + * The setting name. + * + * @return mixed|null + * The value of the setting, or NULL if absent. + */ + protected function getSettingFromFormState(FormStateInterface $form_state, $setting) { + $field_name = $this->fieldDefinition->getName(); + if ($form_state->hasValue(['fields', $field_name, 'settings_edit_form', 'settings', $setting])) { + return $form_state->getValue(['fields', $field_name, 'settings_edit_form', 'settings', $setting]); + } + return $this->getSetting($setting); + } + +} diff --git a/core/modules/field/src/Tests/EntityReference/EntityReferenceAdminTest.php b/core/modules/field/src/Tests/EntityReference/EntityReferenceAdminTest.php index fa68cb6..ea45101 100644 --- a/core/modules/field/src/Tests/EntityReference/EntityReferenceAdminTest.php +++ b/core/modules/field/src/Tests/EntityReference/EntityReferenceAdminTest.php @@ -373,6 +373,7 @@ public function testAvailableFormatters() { $this->assertFieldSelectOptions('fields[field_' . $taxonomy_term_field_name . '][type]', [ 'entity_reference_label', 'entity_reference_entity_id', + 'entity_reference_inline_settings', 'entity_reference_rss_category', 'entity_reference_entity_view', ]); @@ -384,6 +385,7 @@ public function testAvailableFormatters() { 'author', 'entity_reference_entity_id', 'entity_reference_entity_view', + 'entity_reference_inline_settings', 'entity_reference_label', ]); @@ -393,6 +395,7 @@ public function testAvailableFormatters() { 'entity_reference_label', 'entity_reference_entity_id', 'entity_reference_entity_view', + 'entity_reference_inline_settings', ]); // Test if Date Format Reference Field has the correct formatters. @@ -401,6 +404,7 @@ public function testAvailableFormatters() { $this->assertFieldSelectOptions('fields[field_' . $date_format_field_name . '][type]', [ 'entity_reference_label', 'entity_reference_entity_id', + 'entity_reference_inline_settings', ]); } diff --git a/core/modules/field/tests/src/Kernel/EntityReference/EntityReferenceFormatterTest.php b/core/modules/field/tests/src/Kernel/EntityReference/EntityReferenceFormatterTest.php index 1473375..1938781 100644 --- a/core/modules/field/tests/src/Kernel/EntityReference/EntityReferenceFormatterTest.php +++ b/core/modules/field/tests/src/Kernel/EntityReference/EntityReferenceFormatterTest.php @@ -4,8 +4,10 @@ use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheableMetadata; +use Drupal\Core\Entity\Entity\EntityViewDisplay; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\Field\Plugin\Field\FieldFormatter\EntityReferenceEntityFormatter; +use Drupal\entity_test\Entity\EntityTest; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\field\Tests\EntityReference\EntityReferenceTestTrait; @@ -60,6 +62,9 @@ class EntityReferenceFormatterTest extends EntityKernelTestBase { */ protected $unsavedReferencedEntity; + /** + * {@inheritdoc} + */ protected function setUp() { parent::setUp(); @@ -402,6 +407,129 @@ public function testLabelFormatter() { } /** + * Tests entity_reference_inline_settings formatter output. + * + * @param string|null $label_option + * The value for the "label" formatter option. + * @param string $expected_output + * The expected output. + * + * @dataProvider providerTestRenderInlineSettingsFormatter + */ + public function testRenderInlineSettingsFormatter($label_option, $expected_output) { + + $field_storage = FieldStorageConfig::create([ + 'field_name' => 'test_er_field', + 'entity_type' => 'entity_test', + 'type' => 'entity_reference', + 'settings' => [ + 'target_type' => 'entity_test', + ], + ]); + $field_storage->save(); + + $field_config = FieldConfig::create([ + 'field_name' => 'test_er_field', + 'entity_type' => 'entity_test', + 'bundle' => 'entity_test', + ]); + $field_config->save(); + + $parent_entity_view_display = EntityViewDisplay::load('entity_test.entity_test.default'); + $parent_entity_view_display->setComponent('test_er_field', [ + 'type' => 'entity_reference_inline_settings', + 'settings' => [ + 'field_name' => 'name', + 'type' => 'string', + 'settings' => [], + 'label' => $label_option, + 'show_basefields' => FALSE, + ], + ])->save(); + + $child_entity = EntityTest::create([ + 'name' => ['child name'], + ]); + $child_entity->save(); + + $entity = EntityTest::create([ + 'test_er_field' => [[ + 'target_id' => $child_entity->id(), + ], + ], + ]); + $entity->save(); + + $build = $parent_entity_view_display->build($entity); + + $this->container->get('renderer')->renderRoot($build); + + $this->assertEquals($expected_output, $build['test_er_field']['#markup']); + } + + /** + * Data provider for ::testRenderInlineSettingsFormatter(). + */ + public function providerTestRenderInlineSettingsFormatter() { + $output_with_label = << +
test_er_field
+
+
+
Name
+
child name
+
+
+ + +EXPECTED; + $output_with_label_inline = << +
test_er_field
+
+
+
Name
+
child name
+
+
+ + +EXPECTED; + $output_label_hidden = << +
test_er_field
+
+
child name
+
+ + +EXPECTED; + $output_label_visually_hidden = << +
test_er_field
+
+
+
Name
+
child name
+
+
+ + +EXPECTED; + + return [ + ['above', $output_with_label], + ['inline', $output_with_label_inline], + ['hidden', $output_label_hidden], + ['visually_hidden', $output_label_visually_hidden], + ]; + } + + /** * Sets field values and returns a render array as built by * \Drupal\Core\Field\FieldItemListInterface::view(). *