Skip to content
Snippets Groups Projects
Verified Commit be46dafb authored by Lauri Timmanee's avatar Lauri Timmanee
Browse files

Issue #1038316 by tacituseu, vasike, heddn, Eyal Shalev, cchoe1, lauriii,...

Issue #1038316 by tacituseu, vasike, heddn, Eyal Shalev, cchoe1, lauriii, siva01, perry.franken, sanfair, jan.stoeckler, anmolgoyal74, MRPRAVIN, scott.whittaker, dj1999, vytch, SpadXIII, GRO, samuel.seide, szato, Denisev, Munavijayalakshmi, ankithashetty, neclimdul, mayurjadhav, rgpublic, bartzaalberg, electrokate, Lal_, realityloop, ameymudras, linhnm, fabianfiorotto, Abhijith S, bleen, smustgrave, rogueturnip, alexpott: Allow for deletion of a single value of a multiple value field
parent 88007051
No related branches found
No related tags found
45 merge requests!12227Issue #3181946 by jonmcl, mglaman,!54479.5.x SF update,!5014Issue #3071143: Table Render Array Example Is Incorrect,!4868Issue #1428520: Improve menu parent link selection,!4594Applying patch for Views Global Text area field to allow extra HTML tags. As video, source and iframe tag is not rendering. Due to which Media embedded video and remote-video not rendering in Views Global Text area field.,!3878Removed unused condition head title for views,!38582585169-10.1.x,!3818Issue #2140179: $entity->original gets stale between updates,!3742Issue #3328429: Create item list field formatter for displaying ordered and unordered lists,!3731Claro: role=button on status report items,!3668Resolve #3347842 "Deprecate the trusted",!3651Issue #3347736: Create new SDC component for Olivero (header-search),!3546refactored dialog.pcss file,!3531Issue #3336994: StringFormatter always displays links to entity even if the user in context does not have access,!3502Issue #3335308: Confusing behavior with FormState::setFormState and FormState::setMethod,!3478Issue #3337882: Deleted menus are not removed from content type config,!3452Issue #3332701: Refactor Claro's tablesort-indicator stylesheet,!3451Issue #2410579: Allows setting the current language programmatically.,!3355Issue #3209129: Scrolling problems when adding a block via layout builder,!3226Issue #2987537: Custom menu link entity type should not declare "bundle" entity key,!3154Fixes #2987987 - CSRF token validation broken on routes with optional parameters.,!3147Issue #3328457: Replace most substr($a, $i) where $i is negative with str_ends_with(),!3146Issue #3328456: Replace substr($a, 0, $i) with str_starts_with(),!3133core/modules/system/css/components/hidden.module.css,!31312878513-10.1.x,!2964Issue #2865710 : Dependencies from only one instance of a widget are used in display modes,!2812Issue #3312049: [Followup] Fix Drupal.Commenting.FunctionComment.MissingReturnType returns for NULL,!2614Issue #2981326: Replace non-test usages of \Drupal::logger() with IoC injection,!2378Issue #2875033: Optimize joins and table selection in SQL entity query implementation,!2334Issue #3228209: Add hasRole() method to AccountInterface,!2062Issue #3246454: Add weekly granularity to views date sort,!1591Issue #3199697: Add JSON:API Translation experimental module,!1255Issue #3238922: Refactor (if feasible) uses of the jQuery serialize function to use vanillaJS,!1105Issue #3025039: New non translatable field on translatable content throws error,!1073issue #3191727: Focus states on mobile second level navigation items fixed,!10223132456: Fix issue where views instances are emptied before an ajax request is complete,!877Issue #2708101: Default value for link text is not saved,!844Resolve #3036010 "Updaters",!673Issue #3214208: FinishResponseSubscriber could create duplicate headers,!579Issue #2230909: Simple decimals fail to pass validation,!560Move callback classRemove outside of the loop,!555Issue #3202493,!485Sets the autocomplete attribute for username/password input field on login form.,!213Issue #2906496: Give Media a menu item under Content,!30Issue #3182188: Updates composer usage to point at ./vendor/bin/composer
......@@ -1629,6 +1629,7 @@ function template_preprocess_field_multiple_value_form(&$variables) {
'colspan' => 2,
'class' => ['field-label'],
],
[],
t('Order', [], ['context' => 'Sort order']),
];
$rows = [];
......@@ -1656,9 +1657,17 @@ function template_preprocess_field_multiple_value_form(&$variables) {
$delta_element = $item['_weight'];
unset($item['_weight']);
// Render actions in a separate column.
$actions = [];
if (isset($item['_actions'])) {
$actions = $item['_actions'];
unset($item['_actions']);
}
$cells = [
['data' => '', 'class' => ['field-multiple-drag']],
['data' => $item],
['data' => $actions],
['data' => $delta_element, 'class' => ['delta-order']],
];
$rows[] = [
......
......@@ -169,6 +169,8 @@ public function form(FieldItemListInterface $items, array &$form, FormStateInter
protected function formMultipleElements(FieldItemListInterface $items, array &$form, FormStateInterface $form_state) {
$field_name = $this->fieldDefinition->getName();
$cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality();
$is_multiple = $this->fieldDefinition->getFieldStorageDefinition()->isMultiple();
$is_unlimited_not_programmed = FALSE;
$parents = $form['#parents'];
// Determine the number of widgets to display.
......@@ -176,17 +178,18 @@ protected function formMultipleElements(FieldItemListInterface $items, array &$f
case FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED:
$field_state = static::getWidgetState($parents, $field_name, $form_state);
$max = $field_state['items_count'];
$is_multiple = TRUE;
$is_unlimited_not_programmed = !$form_state->isProgrammed();
break;
default:
$max = $cardinality - 1;
$is_multiple = ($cardinality > 1);
break;
}
$title = $this->fieldDefinition->getLabel();
$description = $this->getFilteredDescription();
$id_prefix = implode('-', array_merge($parents, [$field_name]));
$wrapper_id = Html::getUniqueId($id_prefix . '-add-more-wrapper');
$elements = [];
......@@ -229,6 +232,29 @@ protected function formMultipleElements(FieldItemListInterface $items, array &$f
'#default_value' => $items[$delta]->_weight ?: $delta,
'#weight' => 100,
];
// Add 'remove' button, if not working with a programmed form.
if ($is_unlimited_not_programmed) {
$remove_button = [
'#delta' => $delta,
'#name' => str_replace('-', '_', $id_prefix) . "_{$delta}_remove_button",
'#type' => 'submit',
'#value' => $this->t('Remove'),
'#validate' => [],
'#submit' => [[static::class, 'deleteSubmit']],
'#limit_validation_errors' => [],
'#ajax' => [
'callback' => [static::class, 'deleteAjax'],
'wrapper' => $wrapper_id,
'effect' => 'fade',
],
];
$element['_actions'] = [
'delete' => $remove_button,
'#weight' => 101,
];
}
}
$elements[$delta] = $element;
......@@ -240,7 +266,7 @@ protected function formMultipleElements(FieldItemListInterface $items, array &$f
'#theme' => 'field_multiple_value_form',
'#field_name' => $field_name,
'#cardinality' => $cardinality,
'#cardinality_multiple' => $this->fieldDefinition->getFieldStorageDefinition()->isMultiple(),
'#cardinality_multiple' => $is_multiple,
'#required' => $this->fieldDefinition->isRequired(),
'#title' => $title,
'#description' => $description,
......@@ -248,9 +274,7 @@ protected function formMultipleElements(FieldItemListInterface $items, array &$f
];
// Add 'add more' button, if not working with a programmed form.
if ($cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED && !$form_state->isProgrammed()) {
$id_prefix = implode('-', array_merge($parents, [$field_name]));
$wrapper_id = Html::getUniqueId($id_prefix . '-add-more-wrapper');
if ($is_unlimited_not_programmed) {
$elements['#prefix'] = '<div id="' . $wrapper_id . '">';
$elements['#suffix'] = '</div>';
......@@ -334,6 +358,82 @@ public static function addMoreAjax(array $form, FormStateInterface $form_state)
return $element;
}
/**
* Ajax submit callback for the "Remove" button.
*
* This re-numbers form elements and removes an item.
*
* @param array $form
* The form array to remove elements from.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public static function deleteSubmit(&$form, FormStateInterface $form_state) {
$button = $form_state->getTriggeringElement();
$delta = (int) $button['#delta'];
$array_parents = array_slice($button['#array_parents'], 0, -4);
$parent_element = NestedArray::getValue($form, array_merge($array_parents, ['widget']));
$field_name = $parent_element['#field_name'];
$parents = $parent_element['#field_parents'];
$field_state = static::getWidgetState($parents, $field_name, $form_state);
$user_input = $form_state->getUserInput();
$field_input = NestedArray::getValue($user_input, $parent_element['#parents'], $exists);
if ($exists) {
$field_values = [];
foreach ($field_input as $key => $input) {
if (is_numeric($key) && $key >= $delta) {
if ((int) $key === $delta) {
--$key;
continue;
}
}
$field_values[$key] = $input;
}
NestedArray::setValue($user_input, $parent_element['#parents'], $field_values);
$form_state->setUserInput($user_input);
}
unset($parent_element[$delta]);
NestedArray::setValue($form, $array_parents, $parent_element);
if ($field_state['items_count'] > 0) {
$field_state['items_count']--;
}
$user_input = $form_state->getUserInput();
$input = NestedArray::getValue($user_input, $parent_element['#parents'], $exists);
$weight = -1 * $field_state['items_count'];
foreach ($input as $key => $item) {
if ($item) {
$input[$key]['_weight'] = $weight++;
}
}
// Reset indices.
$input = array_values($input);
$user_input = $form_state->getUserInput();
NestedArray::setValue($user_input, $parent_element['#parents'], $input);
$form_state->setUserInput($user_input);
static::setWidgetState($parents, $field_name, $form_state, $field_state);
$form_state->setRebuild();
}
/**
* Ajax refresh callback for the "Remove" button.
*
* This returns the new widget element content to replace
* the previous content made obsolete by the form submission.
*
* @param array $form
* The form array to remove elements from.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public static function deleteAjax(array &$form, FormStateInterface $form_state) {
$button = $form_state->getTriggeringElement();
return NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -3));
}
/**
* Generates the form element for a single copy of the widget.
*/
......@@ -402,7 +502,7 @@ public function extractFormValues(FieldItemListInterface $items, array $form, Fo
$field_state = static::getWidgetState($form['#parents'], $field_name, $form_state);
foreach ($items as $delta => $item) {
$field_state['original_deltas'][$delta] = $item->_original_delta ?? $delta;
unset($item->_original_delta, $item->_weight);
unset($item->_original_delta, $item->_weight, $item->_actions);
}
static::setWidgetState($form['#parents'], $field_name, $form_state, $field_state);
}
......
......@@ -9,11 +9,11 @@
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
/**
* Tests add more behavior for a multiple value field.
* Tests widget form for a multiple value field.
*
* @group field
*/
class FormJSAddMoreTest extends WebDriverTestBase {
class MultipleValueWidgetTest extends WebDriverTestBase {
/**
* {@inheritdoc}
......@@ -64,7 +64,7 @@ protected function setUp(): void {
/**
* Tests the 'Add more' functionality.
*/
public function testFieldFormJsAddMore() {
public function testFieldMultipleValueWidget() {
$this->drupalGet('entity_test/add');
$assert_session = $this->assertSession();
......@@ -75,11 +75,17 @@ public function testFieldFormJsAddMore() {
$field_0 = $page->findField('field_unlimited[0][value]');
$field_0->setValue('1');
$field_0_remove_button = $page->findButton('field_unlimited_0_remove_button');
$this->assertNotEmpty($field_0_remove_button, 'First field has a remove button.');
// Add another item.
$add_more_button->click();
$field_1 = $assert_session->waitForField('field_unlimited[1][value]');
$this->assertNotEmpty($field_1, 'Successfully added another item.');
$field_1_remove_button = $page->findButton('field_unlimited_1_remove_button');
$this->assertNotEmpty($field_1_remove_button, 'Also second field has a remove button.');
// Validate the value of the first field has not changed.
$this->assertEquals('1', $field_0->getValue(), 'Value for the first item has not changed.');
......@@ -123,6 +129,27 @@ public function testFieldFormJsAddMore() {
// Validate no extraneous widget is displayed.
$element = $page->findField('field_unlimited[4][value]');
$this->assertEmpty($element);
// Test removing items/values.
$field_0_remove_button->click();
$this->assertSession()->assertWaitOnAjaxRequest();
// Test the updated widget.
// First item is the initial second item.
$this->assertEquals('2', $field_0->getValue(), 'Value for the first item has changed.');
// We do not have the initial first item anymore.
$this->assertEmpty($field_2->getValue(), 'Value for the third item is currently empty.');
$element = $page->findField('field_unlimited[3][value]');
$this->assertEmpty($element);
// We can also remove empty items.
$field_2_remove_button = $page->findButton('field_unlimited_2_remove_button');
$field_2_remove_button->click();
$this->assertSession()->assertWaitOnAjaxRequest();
$element = $page->findField('field_unlimited[2][value]');
$this->assertEmpty($element, 'Empty field also removed.');
// Assert that the wrapper exists and isn't nested.
$this->assertSession()->elementsCount('css', '[data-drupal-selector="edit-field-unlimited-wrapper"]', 1);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment