diff --git a/core/config/schema/core.entity.schema.yml b/core/config/schema/core.entity.schema.yml index 2934ca7fee0c1287bed1f4bae8092fad68fe986c..2bf4357c668107602ede034153b59308c571e385 100644 --- a/core/config/schema/core.entity.schema.yml +++ b/core/config/schema/core.entity.schema.yml @@ -174,11 +174,18 @@ field.formatter.settings.*: # Default schema for entity form display field with undefined type. field.widget.settings.*: type: mapping + mapping: + orderable: + type: boolean + label: 'Orderable' field.widget.settings.string_textfield: type: mapping label: 'Text field display format settings' mapping: + orderable: + type: boolean + label: 'Orderable' size: type: integer label: 'Size of textfield' @@ -190,6 +197,9 @@ field.widget.settings.string_textarea: type: mapping label: 'Textarea display format settings' mapping: + orderable: + type: boolean + label: 'Orderable' rows: type: integer label: 'Rows' @@ -201,6 +211,9 @@ field.widget.settings.uri: type: mapping label: 'URI field' mapping: + orderable: + type: boolean + label: 'Orderable' size: type: integer label: 'Size of URI field' @@ -212,6 +225,9 @@ field.widget.settings.email_default: type: mapping label: 'Email field display format settings' mapping: + orderable: + type: boolean + label: 'Orderable' placeholder: type: label label: 'Placeholder' @@ -222,11 +238,18 @@ field.widget.settings.email_default: field.widget.settings.datetime_timestamp: type: mapping label: 'Datetime timestamp display format settings' + mapping: + orderable: + type: boolean + label: 'Orderable' field.widget.settings.boolean_checkbox: type: mapping label: 'Boolean checkbox display format settings' mapping: + orderable: + type: boolean + label: 'Orderable' display_label: type: boolean label: 'Display label' @@ -239,6 +262,9 @@ field.widget.settings.number: type: mapping label: 'Number default display format settings' mapping: + orderable: + type: boolean + label: 'Orderable' placeholder: type: label label: 'Placeholder' @@ -247,6 +273,9 @@ field.widget.settings.checkbox: type: mapping label: 'Single on/off checkbox format settings' mapping: + orderable: + type: boolean + label: 'Orderable' display_label: type: boolean label: 'Use field label instead of the "On value" as label' @@ -255,6 +284,9 @@ field.widget.settings.language_select: type: mapping label: 'Language format settings' mapping: + orderable: + type: boolean + label: 'Orderable' include_locked: type: boolean label: 'Include locked languages' @@ -263,6 +295,9 @@ field.widget.settings.entity_reference_autocomplete_tags: type: mapping label: 'Entity reference autocomplete (Tags style) display format settings' mapping: + orderable: + type: boolean + label: 'Orderable' match_operator: type: string label: 'Autocomplete matching' @@ -280,6 +315,9 @@ field.widget.settings.entity_reference_autocomplete: type: mapping label: 'Entity reference autocomplete display format settings' mapping: + orderable: + type: boolean + label: 'Orderable' match_operator: type: string label: 'Autocomplete matching' diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 48bed90cd70a959ecf719d6dcc4e21875282a0ef..f1f089cfa6e8bb21af53a66ba7bcc1409364482b 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -1541,6 +1541,93 @@ function template_preprocess_field_multiple_value_form(&$variables): void { } } +/** + * Prepares variables for individual form element templates. + * + * Default template: field-multiple-value-without-order-form.html.twig. + * + * Combines multiple values. + * + * @param array $variables + * An associative array containing: + * - element: A render element representing the form element. + */ +function template_preprocess_field_multiple_value_without_order_form(&$variables): void { + $element = $variables['element']; + $variables['multiple'] = $element['#cardinality_multiple']; + + if ($variables['multiple']) { + $items = []; + $variables['button'] = []; + foreach (Element::children($element) as $key) { + if ($key === 'add_more') { + $variables['button'] = &$element[$key]; + } + else { + $items[$key] = &$element[$key]; + if (isset($items[$key]['_weight'])) { + $items[$key]['_weight']['#access'] = FALSE; + } + } + } + usort($items, '_field_multiple_value_form_sort_helper'); + + if (isset($element['#file_upload_title'])) { + // @see template_preprocess_file_widget_multiple() + foreach ($items as $key => &$widget) { + // Save the uploading row for last. + if (empty($widget['#files'])) { + $widget['#title'] = $element['#file_upload_title']; + $widget['#title_display'] = 'hidden'; + $widget['#description'] = \Drupal::service('renderer')->renderInIsolation($element['#file_upload_description']); + continue; + } + + // Delay rendering of the "Display" option and the weight selector, so + // that each can be rendered later in its own column. + if ($element['#display_field']) { + hide($widget['display']); + } + + // Render everything else together in a column, without the normal + // wrappers. + $widget['#theme_wrappers'] = []; + if ($element['#display_field']) { + unset($widget['display']['#title']); + } + } + } + + $variables['title'] = []; + if (!empty($element['#title'])) { + $variables['title'] = [ + '#type' => 'label', + '#title' => $element['#title'], + '#required' => !empty($element['#required']) ? $element['#required'] : FALSE, + '#title_display' => 'before', + ]; + } + $variables['items'] = $items; + + if (!empty($element['#description'])) { + $variables['description_display'] = $element['#description_display']; + $description_id = $element['#attributes']['aria-describedby']; + $description_attributes['id'] = $description_id; + $variables['description']['attributes'] = new Attribute($description_attributes); + $variables['description']['content'] = $element['#description']; + + // Add the description's id to the items aria attributes. + $variables['items']['#attributes']['aria-describedby'] = $element['#attributes']['aria-describedby']; + } + } + else { + $variables['elements'] = []; + foreach (Element::children($element) as $key) { + $variables['elements'][] = $element[$key]; + } + } +} + /** * Prepares variables for breadcrumb templates. * diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/BooleanCheckboxWidget.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/BooleanCheckboxWidget.php index b0f968124c41f90d153ac2cf91c9eaf56e63a5a4..5c7968caf7fdb982b12aa230b2f45daa6297ffce 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/BooleanCheckboxWidget.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/BooleanCheckboxWidget.php @@ -32,6 +32,8 @@ public static function defaultSettings() { * {@inheritdoc} */ public function settingsForm(array $form, FormStateInterface $form_state) { + $element = parent::settingsForm($form, $form_state); + $element['display_label'] = [ '#type' => 'checkbox', '#title' => $this->t('Use field label instead of the "On" label as the label.'), @@ -45,7 +47,7 @@ public function settingsForm(array $form, FormStateInterface $form_state) { * {@inheritdoc} */ public function settingsSummary() { - $summary = []; + $summary = parent::settingsSummary(); $display_label = $this->getSetting('display_label'); $summary[] = $this->t('Use field label: @display_label', ['@display_label' => ($display_label ? $this->t('Yes') : $this->t('No'))]); diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/EmailDefaultWidget.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/EmailDefaultWidget.php index 75c2b1097d43c14c4d92315b7807867a45496990..1f7bcec4f1f60c46d0337b05c253e052e71bff41 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/EmailDefaultWidget.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/EmailDefaultWidget.php @@ -33,6 +33,8 @@ public static function defaultSettings() { * {@inheritdoc} */ public function settingsForm(array $form, FormStateInterface $form_state) { + $element = parent::settingsForm($form, $form_state); + $element['size'] = [ '#type' => 'number', '#title' => $this->t('Textfield size'), @@ -53,7 +55,7 @@ public function settingsForm(array $form, FormStateInterface $form_state) { * {@inheritdoc} */ public function settingsSummary() { - $summary = []; + $summary = parent::settingsSummary(); $placeholder = $this->getSetting('placeholder'); if (!empty($placeholder)) { diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/EntityReferenceAutocompleteWidget.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/EntityReferenceAutocompleteWidget.php index f373ac800b828fe1c60d1daf1729b928c75abe5a..3c38dde148d71e394767543d81bfc9c0aa784261 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/EntityReferenceAutocompleteWidget.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/EntityReferenceAutocompleteWidget.php @@ -37,6 +37,8 @@ public static function defaultSettings() { * {@inheritdoc} */ public function settingsForm(array $form, FormStateInterface $form_state) { + $element = parent::settingsForm($form, $form_state); + $element['match_operator'] = [ '#type' => 'radios', '#title' => $this->t('Autocomplete matching'), @@ -71,7 +73,7 @@ public function settingsForm(array $form, FormStateInterface $form_state) { * {@inheritdoc} */ public function settingsSummary() { - $summary = []; + $summary = parent::settingsSummary(); $operators = $this->getMatchOperatorOptions(); $summary[] = $this->t('Autocomplete matching: @match_operator', ['@match_operator' => $operators[$this->getSetting('match_operator')]]); diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/NumberWidget.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/NumberWidget.php index e827bfd999e66c0572ea3dabbefa545784c375e9..82d3ea7c4adc401e914b10c8d59ae7d7ac628361 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/NumberWidget.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/NumberWidget.php @@ -37,6 +37,8 @@ public static function defaultSettings() { * {@inheritdoc} */ public function settingsForm(array $form, FormStateInterface $form_state) { + $element = parent::settingsForm($form, $form_state); + $element['placeholder'] = [ '#type' => 'textfield', '#title' => $this->t('Placeholder'), @@ -50,7 +52,7 @@ public function settingsForm(array $form, FormStateInterface $form_state) { * {@inheritdoc} */ public function settingsSummary() { - $summary = []; + $summary = parent::settingsSummary(); $placeholder = $this->getSetting('placeholder'); if (!empty($placeholder)) { diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/StringTextareaWidget.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/StringTextareaWidget.php index cc550c2f03c77144d1be5ac639cb5a836352b176..cb5a13d0d54a81abe963aa05351766677ea3d6fa 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/StringTextareaWidget.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/StringTextareaWidget.php @@ -32,6 +32,8 @@ public static function defaultSettings() { * {@inheritdoc} */ public function settingsForm(array $form, FormStateInterface $form_state) { + $element = parent::settingsForm($form, $form_state); + $element['rows'] = [ '#type' => 'number', '#title' => $this->t('Rows'), @@ -52,7 +54,7 @@ public function settingsForm(array $form, FormStateInterface $form_state) { * {@inheritdoc} */ public function settingsSummary() { - $summary = []; + $summary = parent::settingsSummary(); $summary[] = $this->t('Number of rows: @rows', ['@rows' => $this->getSetting('rows')]); $placeholder = $this->getSetting('placeholder'); diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/StringTextfieldWidget.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/StringTextfieldWidget.php index 1523e4f618f72b2487657b94486b549b91f667df..e86799f4ac3373bf346fd33f513d825e90ff9161 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/StringTextfieldWidget.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/StringTextfieldWidget.php @@ -32,6 +32,8 @@ public static function defaultSettings() { * {@inheritdoc} */ public function settingsForm(array $form, FormStateInterface $form_state) { + $element = parent::settingsForm($form, $form_state); + $element['size'] = [ '#type' => 'number', '#title' => $this->t('Size of textfield'), @@ -52,7 +54,7 @@ public function settingsForm(array $form, FormStateInterface $form_state) { * {@inheritdoc} */ public function settingsSummary() { - $summary = []; + $summary = parent::settingsSummary(); $summary[] = $this->t('Textfield size: @size', ['@size' => $this->getSetting('size')]); $placeholder = $this->getSetting('placeholder'); diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/UriWidget.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/UriWidget.php index 3052ca3039ac3dd0c72eb8b4b1d2864c2c79acdb..e5f95b979d9e396abdf646313fa3eda155ac215b 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/UriWidget.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldWidget/UriWidget.php @@ -32,6 +32,8 @@ public static function defaultSettings() { * {@inheritdoc} */ public function settingsForm(array $form, FormStateInterface $form_state) { + $element = parent::settingsForm($form, $form_state); + $element['size'] = [ '#type' => 'number', '#title' => $this->t('Size of URI field'), @@ -52,7 +54,7 @@ public function settingsForm(array $form, FormStateInterface $form_state) { * {@inheritdoc} */ public function settingsSummary() { - $summary = []; + $summary = parent::settingsSummary(); $summary[] = $this->t('URI field size: @size', ['@size' => $this->getSetting('size')]); $placeholder = $this->getSetting('placeholder'); diff --git a/core/lib/Drupal/Core/Field/WidgetBase.php b/core/lib/Drupal/Core/Field/WidgetBase.php index 6b408991cc0243ba47829d500aa9602ba925057c..e4b6b228b61f67578c519a9fe71d006061d84347 100644 --- a/core/lib/Drupal/Core/Field/WidgetBase.php +++ b/core/lib/Drupal/Core/Field/WidgetBase.php @@ -266,7 +266,7 @@ protected function formMultipleElements(FieldItemListInterface $items, array &$f if ($elements) { $elements += [ - '#theme' => 'field_multiple_value_form', + '#theme' => $this->getSetting('orderable') ? 'field_multiple_value_form' : 'field_multiple_value_without_order_form', '#field_name' => $field_name, '#cardinality' => $cardinality, '#cardinality_multiple' => $is_multiple, @@ -631,18 +631,45 @@ protected static function getWidgetStateParents(array $parents, $field_name) { return array_merge(['field_storage', '#parents'], $parents, ['#fields', $field_name]); } + /** + * {@inheritdoc} + */ + public static function defaultSettings() { + return [ + 'orderable' => TRUE, + ] + parent::defaultSettings(); + } + /** * {@inheritdoc} */ public function settingsForm(array $form, FormStateInterface $form_state) { - return []; + $element = []; + if ($this->fieldDefinition->getFieldStorageDefinition()->getCardinality() != 1) { + $element['orderable'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Orderable'), + '#default_value' => $this->getSetting('orderable'), + '#weight' => -5, + '#description' => $this->t('Orderable multiple fields widgets are in a table with drag and drop.'), + ]; + } + + return $element; } /** * {@inheritdoc} */ public function settingsSummary() { - return []; + $summary = []; + + if ($this->fieldDefinition->getFieldStorageDefinition()->getCardinality() != 1) { + $orderable = $this->getSetting('orderable'); + $summary[] = $this->t('Orderable: @orderable', ['@orderable' => ($orderable ? $this->t('Yes') : $this->t('No'))]); + } + + return $summary; } /** diff --git a/core/lib/Drupal/Core/Theme/ThemeCommonElements.php b/core/lib/Drupal/Core/Theme/ThemeCommonElements.php index 05d5e168e1a3a3e67580e71284ffa9d22c4a0260..d90d2e9daaee21002b9d51a539f4e58522d34f25 100644 --- a/core/lib/Drupal/Core/Theme/ThemeCommonElements.php +++ b/core/lib/Drupal/Core/Theme/ThemeCommonElements.php @@ -235,6 +235,9 @@ public static function commonElements(): array { 'field_multiple_value_form' => [ 'render element' => 'element', ], + 'field_multiple_value_without_order_form' => [ + 'render element' => 'element', + ], 'off_canvas_page_wrapper' => [ 'variables' => [ 'children' => NULL, diff --git a/core/modules/comment/config/schema/comment.schema.yml b/core/modules/comment/config/schema/comment.schema.yml index 33b3e55ac3ab7ea9f22df95e8f06bcde6e5edd6b..25bab191eb3a64642c821cc2b7fd386cf4ac1614 100644 --- a/core/modules/comment/config/schema/comment.schema.yml +++ b/core/modules/comment/config/schema/comment.schema.yml @@ -23,6 +23,10 @@ field.formatter.settings.comment_default: field.widget.settings.comment_default: type: mapping label: 'Comment display format settings' + mapping: + orderable: + type: boolean + label: 'Orderable' comment.type.*: type: config_entity diff --git a/core/modules/datetime/config/schema/datetime.schema.yml b/core/modules/datetime/config/schema/datetime.schema.yml index a6047ee4e1bd1dc45f121e00aee5a2ab4e30441e..4bd5e1819cd58a4e4be8ebe9012b436dbed9c416 100644 --- a/core/modules/datetime/config/schema/datetime.schema.yml +++ b/core/modules/datetime/config/schema/datetime.schema.yml @@ -66,6 +66,9 @@ field.widget.settings.datetime_datelist: type: mapping label: 'Datetime select list display format settings' mapping: + orderable: + type: boolean + label: 'Orderable' increment: type: integer label: 'Time increments' @@ -79,3 +82,7 @@ field.widget.settings.datetime_datelist: field.widget.settings.datetime_default: type: mapping label: 'Datetime default display format settings' + mapping: + orderable: + type: boolean + label: 'Orderable' diff --git a/core/modules/datetime/src/Plugin/Field/FieldWidget/DateTimeDatelistWidget.php b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateTimeDatelistWidget.php index e7999309d4fbdbf277a05e1f359da2bf5bd00654..79233ae1391f052176d58ff427ab67754e3b1968 100644 --- a/core/modules/datetime/src/Plugin/Field/FieldWidget/DateTimeDatelistWidget.php +++ b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateTimeDatelistWidget.php @@ -126,7 +126,7 @@ public function settingsForm(array $form, FormStateInterface $form_state) { * {@inheritdoc} */ public function settingsSummary() { - $summary = []; + $summary = parent::settingsSummary(); $summary[] = $this->t('Date part order: @order', ['@order' => $this->getSetting('date_order')]); if ($this->getFieldSetting('datetime_type') == 'datetime') { diff --git a/core/modules/datetime_range/config/schema/datetime_range.schema.yml b/core/modules/datetime_range/config/schema/datetime_range.schema.yml index bb244553edc13236dc327592546463f00cadd8cc..40199bdb227f4f0d533ed1fcea1a788ad37ebdfd 100644 --- a/core/modules/datetime_range/config/schema/datetime_range.schema.yml +++ b/core/modules/datetime_range/config/schema/datetime_range.schema.yml @@ -79,6 +79,9 @@ field.widget.settings.daterange_datelist: type: mapping label: 'Date range select list display format settings' mapping: + orderable: + type: boolean + label: 'Orderable' increment: type: integer label: 'Time increments' @@ -92,3 +95,7 @@ field.widget.settings.daterange_datelist: field.widget.settings.daterange_default: type: mapping label: 'Date range default display format settings' + mapping: + orderable: + type: boolean + label: 'Orderable' diff --git a/core/modules/datetime_range/src/Plugin/Field/FieldWidget/DateRangeDatelistWidget.php b/core/modules/datetime_range/src/Plugin/Field/FieldWidget/DateRangeDatelistWidget.php index 0c90959607a9d04f3b7cd9e5b256e49291228493..472d6c3b35b26582da663bfb50d757eef68855d2 100644 --- a/core/modules/datetime_range/src/Plugin/Field/FieldWidget/DateRangeDatelistWidget.php +++ b/core/modules/datetime_range/src/Plugin/Field/FieldWidget/DateRangeDatelistWidget.php @@ -142,7 +142,7 @@ public function settingsForm(array $form, FormStateInterface $form_state) { * {@inheritdoc} */ public function settingsSummary() { - $summary = []; + $summary = parent::settingsSummary(); $summary[] = $this->t('Date part order: @order', ['@order' => $this->getSetting('date_order')]); if ($this->getFieldSetting('datetime_type') == DateRangeItem::DATETIME_TYPE_DATETIME) { diff --git a/core/modules/field/tests/modules/field_test/config/schema/field_test.schema.yml b/core/modules/field/tests/modules/field_test/config/schema/field_test.schema.yml index 5a65b969868624ec03a7dbff3bee7e4dbc5211be..83ae206f6a5fcf73859aada3bf394aa638310ac6 100644 --- a/core/modules/field/tests/modules/field_test/config/schema/field_test.schema.yml +++ b/core/modules/field/tests/modules/field_test/config/schema/field_test.schema.yml @@ -37,6 +37,9 @@ field.widget.settings.test_field_widget: type: mapping label: 'Test field widget settings' mapping: + orderable: + type: boolean + label: 'Orderable' test_widget_setting: type: string label: 'Test setting' @@ -51,6 +54,9 @@ field.widget.settings.test_field_widget_multiple: type: mapping label: 'Test multiple field widget settings' mapping: + orderable: + type: boolean + label: 'Orderable' test_widget_setting_multiple: type: string label: 'Test setting' @@ -59,6 +65,9 @@ field.widget.settings.test_field_widget_multiple_single_value: type: mapping label: 'Test multiple field widget settings: single values' mapping: + orderable: + type: boolean + label: 'Orderable' test_widget_setting_multiple: type: string label: 'Test setting' diff --git a/core/modules/field/tests/src/Functional/FormTest.php b/core/modules/field/tests/src/Functional/FormTest.php index e5c8838f353d30dc85e5cffa7d33b4f29f930478..bda7957c573e643eb0cc0d2536518bd196dee02c 100644 --- a/core/modules/field/tests/src/Functional/FormTest.php +++ b/core/modules/field/tests/src/Functional/FormTest.php @@ -94,6 +94,7 @@ protected function setUp(): void { 'description' => '[site:name]_description', 'weight' => mt_rand(0, 127), 'settings' => [ + 'orderable' => TRUE, 'test_field_setting' => $this->randomMachineName(), ], ]; 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 6e2bf60c00c48150e600a64586910490de78c9d2..5cee361853669e09987f4a3241e7025c00bdf4c5 100644 --- a/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldWidgetSettingsTest.php +++ b/core/modules/field/tests/src/Kernel/Migrate/d6/MigrateFieldWidgetSettingsTest.php @@ -44,6 +44,7 @@ public function testWidgetSettings(): void { 'weight' => 1, 'region' => 'content', 'settings' => [ + 'orderable' => TRUE, 'size' => 60, 'placeholder' => '', ], @@ -55,7 +56,7 @@ public function testWidgetSettings(): void { $component = $form_display->getComponent('field_test_two'); $expected['type'] = 'number'; $expected['weight'] = 1; - $expected['settings'] = ['placeholder' => '']; + $expected['settings'] = ['orderable' => TRUE, 'placeholder' => '']; $this->assertSame($expected, $component); // Float field. @@ -67,49 +68,52 @@ public function testWidgetSettings(): void { $component = $form_display->getComponent('field_test_email'); $expected['type'] = 'email_default'; $expected['weight'] = 6; - $expected['settings'] = ['placeholder' => '', 'size' => 60]; + $expected['settings'] = ['orderable' => TRUE, 'placeholder' => '', 'size' => 60]; $this->assertSame($expected, $component); // Link field. $component = $form_display->getComponent('field_test_link'); - $this->assertSame('link_default', $component['type']); - $this->assertSame(7, $component['weight']); - $this->assertEmpty(array_filter($component['settings'])); + $expected['type'] = 'link_default'; + $expected['weight'] = 7; + $expected['settings'] = ['orderable' => TRUE, 'placeholder_url' => '', 'placeholder_title' => '']; + $this->assertSame($expected, $component); // File field. $component = $form_display->getComponent('field_test_filefield'); $expected['type'] = 'file_generic'; $expected['weight'] = 8; - $expected['settings'] = ['progress_indicator' => 'bar']; + $expected['settings'] = ['orderable' => TRUE, 'progress_indicator' => 'bar']; $this->assertSame($expected, $component); // Image field. $component = $form_display->getComponent('field_test_imagefield'); $expected['type'] = 'image_image'; $expected['weight'] = 9; - $expected['settings'] = ['progress_indicator' => 'bar', 'preview_image_style' => 'thumbnail']; + $expected['settings'] = ['orderable' => TRUE, 'progress_indicator' => 'bar', 'preview_image_style' => 'thumbnail']; $this->assertSame($expected, $component); // Phone field. $component = $form_display->getComponent('field_test_phone'); $expected['type'] = 'telephone_default'; $expected['weight'] = 13; - $expected['settings'] = ['placeholder' => '']; + $expected['settings'] = ['orderable' => TRUE, 'placeholder' => '']; $this->assertSame($expected, $component); // Date fields. $component = $form_display->getComponent('field_test_date'); $expected['type'] = 'datetime_default'; $expected['weight'] = 10; - $expected['settings'] = []; + $expected['settings'] = ['orderable' => TRUE]; $this->assertSame($expected, $component); $component = $form_display->getComponent('field_test_datestamp'); $expected['weight'] = 11; + $expected['settings'] = ['orderable' => TRUE]; $this->assertSame($expected, $component); $component = $form_display->getComponent('field_test_datetime'); $expected['weight'] = 12; + $expected['settings'] = ['orderable' => TRUE]; $this->assertSame($expected, $component); /** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */ diff --git a/core/modules/file/config/schema/file.schema.yml b/core/modules/file/config/schema/file.schema.yml index f00ab3d23d7181a859cef8a9ae7e53c6187f5814..d76c537e61c06a6a51598bbe66af7df39ebfe986 100644 --- a/core/modules/file/config/schema/file.schema.yml +++ b/core/modules/file/config/schema/file.schema.yml @@ -174,6 +174,9 @@ field.widget.settings.file_generic: type: mapping label: 'File format settings' mapping: + orderable: + type: boolean + label: 'Orderable' progress_indicator: type: string label: 'Progress indicator' diff --git a/core/modules/file/src/Plugin/Field/FieldWidget/FileWidget.php b/core/modules/file/src/Plugin/Field/FieldWidget/FileWidget.php index 62db3ddf1a43e96008fe042c4cf02e23aa09d57d..4dd593ecdad7651bc78351dddae075dde292ad76 100644 --- a/core/modules/file/src/Plugin/Field/FieldWidget/FileWidget.php +++ b/core/modules/file/src/Plugin/Field/FieldWidget/FileWidget.php @@ -60,6 +60,8 @@ public static function defaultSettings() { * {@inheritdoc} */ public function settingsForm(array $form, FormStateInterface $form_state) { + $element = parent::settingsForm($form, $form_state); + $element['progress_indicator'] = [ '#type' => 'radios', '#title' => $this->t('Progress indicator'), @@ -79,7 +81,7 @@ public function settingsForm(array $form, FormStateInterface $form_state) { * {@inheritdoc} */ public function settingsSummary() { - $summary = []; + $summary = parent::settingsSummary(); $summary[] = $this->t('Progress indicator: @progress_indicator', ['@progress_indicator' => $this->getSetting('progress_indicator')]); return $summary; } @@ -173,10 +175,17 @@ protected function formMultipleElements(FieldItemListInterface $items, array &$f // The group of elements all-together need some extra functionality after // building up the full list (like draggable table rows). $elements['#file_upload_delta'] = $delta; - $elements['#type'] = 'details'; $elements['#open'] = TRUE; - $elements['#theme'] = 'file_widget_multiple'; - $elements['#theme_wrappers'] = ['details']; + if ($this->getSetting('orderable')) { + $elements['#type'] = 'details'; + $elements['#theme'] = 'file_widget_multiple'; + $elements['#theme_wrappers'] = ['details']; + } + else { + $elements['#theme'] = 'field_multiple_value_without_order_form'; + $elements['#cardinality_multiple'] = $this->fieldDefinition->getFieldStorageDefinition()->isMultiple(); + } + $elements['#process'] = [[static::class, 'processMultiple']]; $elements['#title'] = $title; diff --git a/core/modules/image/config/schema/image.schema.yml b/core/modules/image/config/schema/image.schema.yml index f805caa378ca822067e3fb36ddf1f9fe43a412b6..4c3997043b53f7f24f0324cb56c292971771c55b 100644 --- a/core/modules/image/config/schema/image.schema.yml +++ b/core/modules/image/config/schema/image.schema.yml @@ -179,6 +179,9 @@ field.widget.settings.image_image: type: mapping label: 'Image field display format settings' mapping: + orderable: + type: boolean + label: 'Orderable' progress_indicator: type: string label: 'Progress indicator' diff --git a/core/modules/jsonapi/tests/src/Functional/EntityFormDisplayTest.php b/core/modules/jsonapi/tests/src/Functional/EntityFormDisplayTest.php index 24a34c6fa2ddff1498e2ec732d4f56974e298a26..ae5eaed0435fe68b88e79e5f1c786444ee4ad3bc 100644 --- a/core/modules/jsonapi/tests/src/Functional/EntityFormDisplayTest.php +++ b/core/modules/jsonapi/tests/src/Functional/EntityFormDisplayTest.php @@ -102,13 +102,16 @@ protected function getExpectedDocument(): array { 'type' => 'datetime_timestamp', 'weight' => 10, 'region' => 'content', - 'settings' => [], + 'settings' => [ + 'orderable' => TRUE, + ], 'third_party_settings' => [], ], 'promote' => [ 'type' => 'boolean_checkbox', 'settings' => [ 'display_label' => TRUE, + 'orderable' => TRUE, ], 'weight' => 15, 'region' => 'content', @@ -120,6 +123,7 @@ protected function getExpectedDocument(): array { 'region' => 'content', 'settings' => [ 'display_label' => TRUE, + 'orderable' => TRUE, ], 'third_party_settings' => [], ], @@ -127,6 +131,7 @@ protected function getExpectedDocument(): array { 'type' => 'boolean_checkbox', 'settings' => [ 'display_label' => TRUE, + 'orderable' => TRUE, ], 'weight' => 16, 'region' => 'content', @@ -137,6 +142,7 @@ protected function getExpectedDocument(): array { 'weight' => -5, 'region' => 'content', 'settings' => [ + 'orderable' => TRUE, 'size' => 60, 'placeholder' => '', ], @@ -146,6 +152,7 @@ protected function getExpectedDocument(): array { 'type' => 'entity_reference_autocomplete', 'weight' => 5, 'settings' => [ + 'orderable' => TRUE, 'match_operator' => 'CONTAINS', 'match_limit' => 10, 'size' => 60, diff --git a/core/modules/link/config/schema/link.schema.yml b/core/modules/link/config/schema/link.schema.yml index e3a1ffab98e5716e7226c29e736445a46bf7bb10..493e3fa217e47b36d789ef03fc0f845f68c0f49e 100644 --- a/core/modules/link/config/schema/link.schema.yml +++ b/core/modules/link/config/schema/link.schema.yml @@ -28,6 +28,9 @@ field.widget.settings.link_default: type: mapping label: 'Link format settings' mapping: + orderable: + type: boolean + label: 'Orderable' placeholder_url: type: string label: 'Placeholder for URL' diff --git a/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php b/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php index 3a045b6f641d65aea17472e1be2bc88e33d135b3..8742e83e6962ce418c442e20f6a6d3ea1fb33aa3 100644 --- a/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php +++ b/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php @@ -403,7 +403,7 @@ public function settingsForm(array $form, FormStateInterface $form_state) { * {@inheritdoc} */ public function settingsSummary() { - $summary = []; + $summary = parent::settingsSummary(); $placeholder_title = $this->getSetting('placeholder_title'); $placeholder_url = $this->getSetting('placeholder_url'); diff --git a/core/modules/media_library/config/schema/media_library.schema.yml b/core/modules/media_library/config/schema/media_library.schema.yml index 312f4ab607189a4bf7f28a4177e4b59b3f8bca77..5d613c4e9695f94bc1ac378bd7a5941da7b6ad70 100644 --- a/core/modules/media_library/config/schema/media_library.schema.yml +++ b/core/modules/media_library/config/schema/media_library.schema.yml @@ -2,6 +2,9 @@ field.widget.settings.media_library_widget: type: mapping label: 'Media library widget settings' mapping: + orderable: + type: boolean + label: 'Orderable' media_types: type: sequence label: 'Allowed media types, in display order' diff --git a/core/modules/media_library/tests/modules/media_library_test_widget/config/schema/media_library_test_widget.schema.yml b/core/modules/media_library/tests/modules/media_library_test_widget/config/schema/media_library_test_widget.schema.yml index 81186253e1ae46cb797329e6a6370d1115d494f3..5957f6b917ef816e064f9cc3a373b8d866dc7083 100644 --- a/core/modules/media_library/tests/modules/media_library_test_widget/config/schema/media_library_test_widget.schema.yml +++ b/core/modules/media_library/tests/modules/media_library_test_widget/config/schema/media_library_test_widget.schema.yml @@ -2,6 +2,9 @@ field.widget.settings.media_library_inception_widget: type: mapping label: 'Media library inception widget settings' mapping: + orderable: + type: boolean + label: 'Orderable' media_types: type: sequence label: 'Allowed media types, in display order' diff --git a/core/modules/options/config/schema/options.schema.yml b/core/modules/options/config/schema/options.schema.yml index 87323eb1d4564d2a0204bde8024754c6469c6098..e89f63bb42090b369a368ccad153a7c79be66bd8 100644 --- a/core/modules/options/config/schema/options.schema.yml +++ b/core/modules/options/config/schema/options.schema.yml @@ -104,10 +104,18 @@ field.formatter.settings.list_key: field.widget.settings.options_buttons: type: mapping label: 'Check boxes/radio buttons format settings' + mapping: + orderable: + type: boolean + label: 'Orderable' field.widget.settings.options_select: type: mapping label: 'Select list format settings' + mapping: + orderable: + type: boolean + label: 'Orderable' views.argument.number_list_field: type: views.argument.numeric diff --git a/core/modules/path/config/schema/path.schema.yml b/core/modules/path/config/schema/path.schema.yml index 849dbe3c81823193c342eafe2e9fa8b365be3a1b..7acad352b96924ed97411f295dddfcae4bc10d99 100644 --- a/core/modules/path/config/schema/path.schema.yml +++ b/core/modules/path/config/schema/path.schema.yml @@ -3,3 +3,7 @@ field.widget.settings.path: type: mapping label: 'Link format settings' + mapping: + orderable: + type: boolean + label: 'Orderable' diff --git a/core/modules/system/templates/field-multiple-value-without-order-form.html.twig b/core/modules/system/templates/field-multiple-value-without-order-form.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..236a9fdc3d98c8ad3003cde039b91e0ef72a5488 --- /dev/null +++ b/core/modules/system/templates/field-multiple-value-without-order-form.html.twig @@ -0,0 +1,65 @@ +{# +/** + * @file + * Theme override for an individual form element. + * + * Available variables for all fields: + * - multiple: Whether there are multiple instances of the field. + * + * Available variables for single cardinality fields: + * - elements: Form elements to be rendered. + * + * Available variables when there are multiple fields. + * - table: Table of field items. + * - description: The description element containing the following properties: + * - content: The description content of the form element. + * - attributes: HTML attributes to apply to the description container. + * - description_display: Description display setting. It can have these values: + * - before: The description is output before the element. + * - after: The description is output after the element. This is the default + * value. + * - invisible: The description is output after the element, hidden visually + * but available to screen readers. + * - button: "Add another item" button. + * + * @see template_preprocess_field_multiple_value_without_order_form() + */ +#} +{% + set title_classes = [ + 'label', + required ? 'js-form-required', + required ? 'form-required', + ] +%} +{% + set description_classes = [ + 'description', + description_display == 'invisible' ? 'visually-hidden', +] +%} +{% if multiple %} + <div class="js-form-item form-item"> + {% if title %} + {{ title }} + {% endif %} + {% if description_display == 'before' and description.content %} + <div{{ description.attributes.addClass(description_classes) }}> + {{ description.content }} + </div> + {% endif %} + {{ items }} + {% if description_display in ['after', 'invisible'] and description.content %} + <div{{ description.attributes.addClass(description_classes) }}> + {{ description.content }} + </div> + {% endif %} + {% if button %} + <div class="clearfix">{{ button }}</div> + {% endif %} + </div> +{% else %} + {% for element in elements %} + {{ element }} + {% endfor %} +{% endif %} diff --git a/core/modules/telephone/config/schema/telephone.schema.yml b/core/modules/telephone/config/schema/telephone.schema.yml index 516cbc3459dd4c02d0ec6d6c706e562eb9da2896..2703617b34bf8fcdfff1f1179d3697de0610cbe8 100644 --- a/core/modules/telephone/config/schema/telephone.schema.yml +++ b/core/modules/telephone/config/schema/telephone.schema.yml @@ -12,6 +12,9 @@ field.widget.settings.telephone_default: type: mapping label: 'Telephone default format settings' mapping: + orderable: + type: boolean + label: 'Orderable' placeholder: type: label label: 'Placeholder' diff --git a/core/modules/telephone/src/Plugin/Field/FieldWidget/TelephoneDefaultWidget.php b/core/modules/telephone/src/Plugin/Field/FieldWidget/TelephoneDefaultWidget.php index a8dcdec775bd299e4f112fc7348adb14f88e7c96..8e9b3275366d2429c4e63b904f8cc03c96c31ac8 100644 --- a/core/modules/telephone/src/Plugin/Field/FieldWidget/TelephoneDefaultWidget.php +++ b/core/modules/telephone/src/Plugin/Field/FieldWidget/TelephoneDefaultWidget.php @@ -32,6 +32,8 @@ public static function defaultSettings() { * {@inheritdoc} */ public function settingsForm(array $form, FormStateInterface $form_state) { + $element = parent::settingsForm($form, $form_state); + $element['placeholder'] = [ '#type' => 'textfield', '#title' => $this->t('Placeholder'), @@ -45,7 +47,7 @@ public function settingsForm(array $form, FormStateInterface $form_state) { * {@inheritdoc} */ public function settingsSummary() { - $summary = []; + $summary = parent::settingsSummary(); $placeholder = $this->getSetting('placeholder'); if (!empty($placeholder)) { diff --git a/core/modules/text/config/schema/text.schema.yml b/core/modules/text/config/schema/text.schema.yml index 8b08b109d4669a11d36c3585473e41429968e0aa..b35b8e01e81aece095b12bbbca8a2078232b4705 100644 --- a/core/modules/text/config/schema/text.schema.yml +++ b/core/modules/text/config/schema/text.schema.yml @@ -124,6 +124,9 @@ field.widget.settings.text_textarea: type: mapping label: 'Text area (multiple rows) display format settings' mapping: + orderable: + type: boolean + label: 'Orderable' rows: type: integer label: 'Rows' @@ -135,6 +138,9 @@ field.widget.settings.text_textarea_with_summary: type: mapping label: 'Text area with a summary display format settings' mapping: + orderable: + type: boolean + label: 'Orderable' rows: type: integer label: 'Rows' @@ -152,6 +158,9 @@ field.widget.settings.text_textfield: type: mapping label: 'Text field display format settings' mapping: + orderable: + type: boolean + label: 'Orderable' size: type: integer label: 'Size of textfield' 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 15cc3308b7afef7b6b99a80be153b14cd836e883..60a5d83f8db33a3b1f2b48d1c7a16c55f9402677 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 @@ -23,6 +23,7 @@ content: weight: 2 region: content settings: + orderable: true rows: 9 summary_rows: 3 placeholder: '' @@ -38,7 +39,8 @@ content: type: datetime_timestamp weight: 10 region: content - settings: { } + settings: + orderable: true third_party_settings: { } field_image: type: image_image @@ -62,13 +64,15 @@ content: type: path weight: 30 region: content - settings: { } + settings: + orderable: true third_party_settings: { } promote: type: boolean_checkbox weight: 15 region: content settings: + orderable: true display_label: true third_party_settings: { } status: @@ -83,6 +87,7 @@ content: weight: 16 region: content settings: + orderable: true display_label: true third_party_settings: { } title: @@ -98,6 +103,7 @@ content: weight: 5 region: content settings: + orderable: true match_operator: CONTAINS match_limit: 10 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 edb853ed3de0c5fcef565eaeb2f5dd266fae50c8..75e0f7d7969a616a1055f3d17753f13e972d60db 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 @@ -17,6 +17,7 @@ content: weight: 31 region: content settings: + orderable: true rows: 9 summary_rows: 3 placeholder: '' @@ -26,19 +27,22 @@ content: type: datetime_timestamp weight: 10 region: content - settings: { } + settings: + orderable: true third_party_settings: { } path: type: path weight: 30 region: content - settings: { } + settings: + orderable: true third_party_settings: { } promote: type: boolean_checkbox weight: 15 region: content settings: + orderable: true display_label: true third_party_settings: { } status: @@ -53,6 +57,7 @@ content: weight: 16 region: content settings: + orderable: true display_label: true third_party_settings: { } title: @@ -60,6 +65,7 @@ content: weight: -5 region: content settings: + orderable: true size: 60 placeholder: '' third_party_settings: { } @@ -68,6 +74,7 @@ content: weight: 5 region: content settings: + orderable: true match_operator: CONTAINS match_limit: 10 size: 60 diff --git a/core/tests/Drupal/FunctionalTests/Rest/EntityFormDisplayResourceTestBase.php b/core/tests/Drupal/FunctionalTests/Rest/EntityFormDisplayResourceTestBase.php index 00afb056689f57c9e0dd2299c2b3c5cefa17a6f9..33c796a33251b530e805d4d4896ad7b27f76f220 100644 --- a/core/tests/Drupal/FunctionalTests/Rest/EntityFormDisplayResourceTestBase.php +++ b/core/tests/Drupal/FunctionalTests/Rest/EntityFormDisplayResourceTestBase.php @@ -69,13 +69,16 @@ protected function getExpectedNormalizedEntity() { 'type' => 'datetime_timestamp', 'weight' => 10, 'region' => 'content', - 'settings' => [], + 'settings' => [ + 'orderable' => TRUE, + ], 'third_party_settings' => [], ], 'promote' => [ 'type' => 'boolean_checkbox', 'settings' => [ 'display_label' => TRUE, + 'orderable' => TRUE, ], 'weight' => 15, 'region' => 'content', @@ -87,6 +90,7 @@ protected function getExpectedNormalizedEntity() { 'region' => 'content', 'settings' => [ 'display_label' => TRUE, + 'orderable' => TRUE, ], 'third_party_settings' => [], ], @@ -94,6 +98,7 @@ protected function getExpectedNormalizedEntity() { 'type' => 'boolean_checkbox', 'settings' => [ 'display_label' => TRUE, + 'orderable' => TRUE, ], 'weight' => 16, 'region' => 'content', @@ -104,6 +109,7 @@ protected function getExpectedNormalizedEntity() { 'weight' => -5, 'region' => 'content', 'settings' => [ + 'orderable' => TRUE, 'size' => 60, 'placeholder' => '', ], @@ -115,6 +121,7 @@ protected function getExpectedNormalizedEntity() { 'settings' => [ 'match_operator' => 'CONTAINS', 'match_limit' => 10, + 'orderable' => TRUE, 'size' => 60, 'placeholder' => '', ], diff --git a/core/themes/claro/templates/field-multiple-value-without-order-form.html.twig b/core/themes/claro/templates/field-multiple-value-without-order-form.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..f4aa75faf281ad5a9d53c0ec0b3ab75c09a33f65 --- /dev/null +++ b/core/themes/claro/templates/field-multiple-value-without-order-form.html.twig @@ -0,0 +1,65 @@ +{# +/** + * @file + * Theme override for an individual form element. + * + * Available variables for all fields: + * - multiple: Whether there are multiple instances of the field. + * + * Available variables for single cardinality fields: + * - elements: Form elements to be rendered. + * + * Available variables when there are multiple fields. + * - table: Table of field items. + * - description: The description element containing the following properties: + * - content: The description content of the form element. + * - attributes: HTML attributes to apply to the description container. + * - description_display: Description display setting. It can have these values: + * - before: The description is output before the element. + * - after: The description is output after the element. This is the default + * value. + * - invisible: The description is output after the element, hidden visually + * but available to screen readers. + * - button: "Add another item" button. + * + * @see template_preprocess_field_multiple_value_without_order_form() + */ +#} +{% + set title_classes = [ + 'label', + required ? 'js-form-required', + required ? 'form-required', + ] +%} +{% + set description_classes = [ + 'form-item__description', + description_display == 'invisible' ? 'visually-hidden', +] +%} +{% if multiple %} + <div class="js-form-item form-item"> + {% if title %} + {{ title }} + {% endif %} + {% if description_display == 'before' and description.content %} + <div{{ description.attributes.addClass(description_classes) }}> + {{ description.content }} + </div> + {% endif %} + {{ items }} + {% if description_display in ['after', 'invisible'] and description.content %} + <div{{ description.attributes.addClass(description_classes) }}> + {{ description.content }} + </div> + {% endif %} + {% if button %} + <div class="clearfix">{{ button }}</div> + {% endif %} + </div> +{% else %} + {% for element in elements %} + {{ element }} + {% endfor %} +{% endif %} diff --git a/core/themes/stable9/templates/form/field-multiple-value-without-order-form.html.twig b/core/themes/stable9/templates/form/field-multiple-value-without-order-form.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..c86c13b796fb39cc51e893d2260292615798be0b --- /dev/null +++ b/core/themes/stable9/templates/form/field-multiple-value-without-order-form.html.twig @@ -0,0 +1,46 @@ +{# +/** + * @file + * Theme override for an individual form element. + * + * Available variables for all fields: + * - multiple: Whether there are multiple instances of the field. + * + * Available variables for single cardinality fields: + * - elements: Form elements to be rendered. + * + * Available variables when there are multiple fields. + * - table: Table of field items. + * - description: The description element containing the following properties: + * - content: The description content of the form element. + * - attributes: HTML attributes to apply to the description container. + * - button: "Add another item" button. + * + * @see template_preprocess_field_multiple_value_without_order_form() + */ +#} +{% + set title_classes = [ + 'label', + required ? 'js-form-required', + required ? 'form-required', + ] +%} +{% if multiple %} + {% if title %} + <h4{{ title_attributes.addClass(title_classes) }}>{{ title }}</h4> + {% endif %} + <div class="js-form-item form-item"> + {{ items }} + {% if description.content %} + <div{{ description.attributes.addClass('description') }} >{{ description.content }}</div> + {% endif %} + {% if button %} + <div class="clearfix">{{ button }}</div> + {% endif %} + </div> +{% else %} + {% for element in elements %} + {{ element }} + {% endfor %} +{% endif %}{# empty Twig template #}