You are here

multiple_fields_remove_button.module in Multiple Fields Remove Button 8

Same filename and directory in other branches
  1. 7 multiple_fields_remove_button.module

In this file we use widget hooks to extend their functionality.

We use drupal hooks and add remove button for fields remove.

File

multiple_fields_remove_button.module
View source
<?php

/**
 * @file
 * In this file we use widget hooks to extend their functionality.
 *
 * We use drupal hooks and add remove button for fields remove.
 */
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Language\Language;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Render\Element;

/**
 * Ajax callback remove field when remove click is trigger.
 *
 * In this callback we will replace field items. Main job
 * to delete field item we will done into submit handler.
 *
 * @param array $form
 *   The complete form structure.
 * @param FormStateInterface $form_state
 *   The current state of the form.
 *
 * @return array
 *   Array element.
 */
function multiple_fields_remove_button_js(array $form, FormStateInterface &$form_state) {
  $button = $form_state
    ->getTriggeringElement();
  $address = array_slice($button['#array_parents'], 0, -2);

  // Go one level up in the form, to the widgets container.
  $parent_element = NestedArray::getValue($form, $address);
  $field_name = $parent_element['#field_name'];
  $parents = $parent_element['#field_parents'];
  $widget_state = WidgetBase::getWidgetState($parents, $field_name, $form_state);

  // Go one level up in the form, to the widgets container.
  $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -3));
  $element['#id'] = $widget_state['wrapper_id'];
  $element['#prefix'] = '<div class="ajax-new-content">' . (isset($element['#prefix']) ? $element['#prefix'] : '');
  $element['#suffix'] = (isset($element['#suffix']) ? $element['#suffix'] : '') . '</div>';
  return $element;
}

/**
 * Ajax callback to empty individual values of limited-cardinality fields.
 *
 * @param array $form
 *   The complete form structure.
 * @param FormStateInterface $form_state
 *   The current state of the form.
 *
 * @return \Drupal\Core\Ajax\AjaxResponse
 *   An Ajax response.
 */
function multiple_fields_remove_button_clear_value_js(array $form, FormStateInterface &$form_state) {
  $button = $form_state
    ->getTriggeringElement();

  // Get the container element.
  $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -3));
  return $element;
}

/**
 * Implements hook_element_info_alter().
 */
function multiple_fields_remove_button_element_info_alter(array &$info) {
  if (isset($info['container'])) {
    $info['container']['#process'][] = 'multiple_fields_remove_button_process_container';
  }
}

/**
 * Add correct wrapper to the element.
 *
 * @param array $element
 *  Reference to the Form API form element we're operating on.
 *
 * @return array
 *   Array element.
 */
function multiple_fields_remove_button_process_container(&$element) {
  if (isset($element['widget']['add_more']['#ajax']['wrapper'])) {
    $children = Element::children($element['widget']);
    $wrapperId = $element['widget']['add_more']['#ajax']['wrapper'];
    foreach ($children as $child) {
      if (isset($element['widget'][$child]['remove_button'])) {
        $element['widget'][$child]['remove_button']['#ajax']['wrapper'] = $wrapperId;
      }
    }
  }
  else {
    if (isset($element['widget'][0]['remove_button'])) {
      $children = Element::children($element['widget']);
      foreach ($children as $child) {
        if (isset($element['widget'][$child]['remove_button'])) {
          $element['widget'][$child]['remove_button']['#ajax']['wrapper'] = $element['#id'];
        }
      }
    }
  }
  return $element;
}

/**
 * Implements hook_field_widget_form_alter().
 */
function multiple_fields_remove_button_field_widget_form_alter(&$element, FormStateInterface $form_state, $context) {

  // Add remove button for the following field types.
  $fieldTypes = [
    'addressfield_standard',
    'address',
    'date_popup',
    'date_text',
    'datetime',
    'decimal',
    'dynamic_entity_reference',
    'email',
    'entity_reference',
    'entity_reference_autocomplete',
    'float',
    'integer',
    'link',
    'location',
    'multiple_selects',
    'number',
    'property_reference',
    'string',
    'string_long',
    'smartdate',
    'telephone',
    'text',
    'text_long',
    'text_with_summary',
    'timestamp',
  ];

  // Add extra types with the help of others modules.
  \Drupal::moduleHandler()
    ->alter('multiple_field_remove_button_field_types', $fieldTypes);

  /** @var \Drupal\Core\Field\FieldItemListInterface $items */
  $items = $context['items'];
  $fieldDefinition = $items
    ->getFieldDefinition();
  $storage = $fieldDefinition
    ->getFieldStorageDefinition();
  $type = $storage
    ->getType();
  $cardinality = $storage
    ->getCardinality();

  // Don't do anything for single-value fields.
  if ($cardinality == 1) {
    return;
  }

  // Don't add 'Remove' button to some field types - issue #2925978.
  $skipTypes = [
    'checkboxes',
  ];

  // Allow other modules extends the list.
  \Drupal::moduleHandler()
    ->alter('multiple_field_remove_button_skip_types', $skipTypes);
  $elementType = isset($element['#type']) ? $element['#type'] : [];

  // Don't add 'Remove' button to some widget types - issue #2925978.
  $skipWidgets = [
    'entity_reference_autocomplete_tags',
    'autocomplete_deluxe',
    'media_library_widget',
    'entity_reference_tree',
  ];

  // Allow other modules extends the list.
  \Drupal::moduleHandler()
    ->alter('multiple_field_remove_button_skip_widgets', $skipWidgets);
  $element_widget = [];
  if ($context['widget'] && $context['widget'] instanceof WidgetBase) {
    $element_widget = $context['widget']
      ->getPluginId() ?? [];
  }

  // Optionally, only include certain fields.
  $includedFields = [];
  \Drupal::moduleHandler()
    ->alter('multiple_field_remove_button_included_fields', $includedFields);
  $is_cardinality_unlimited = $cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED;
  if (in_array($type, $fieldTypes) && !in_array($elementType, $skipTypes) && !in_array($element_widget, $skipWidgets)) {
    $field_parents = isset($element['#field_parents']) ? $element['#field_parents'] : [];
    $field_name = $fieldDefinition
      ->getName();
    if (!empty($includedFields) && !in_array($field_name, $includedFields)) {
      return;
    }
    $language = isset($element['#language']) ? $element['#language'] : Language::LANGCODE_NOT_SPECIFIED;
    $delta = _multiple_fields_remove_button_get_delta($element);

    // Get parent which will we use into Remove Button Element.
    $parents = array_merge($field_parents, [
      $field_name,
      $language,
      $delta,
    ]);
    $remove_btn_name = implode('_', $parents) . '_remove_button';
    $callback = $is_cardinality_unlimited ? 'multiple_fields_remove_button_js' : 'multiple_fields_remove_button_clear_value_js';
    $element['remove_button'] = [
      '#delta' => $delta,
      '#name' => $remove_btn_name,
      '#type' => 'submit',
      '#value' => t('Remove'),
      '#validate' => [],
      '#attributes' => [
        'class' => [
          'multiple-fields-remove-button',
        ],
      ],
      '#submit' => [
        'multiple_fields_remove_button_fixed_submit_handler',
      ],
      '#limit_validation_errors' => [],
      '#ajax' => [
        'callback' => $callback,
      ],
      '#weight' => 1000,
    ];
    if ($is_cardinality_unlimited) {
      $element['remove_button']['#submit'] = [
        'multiple_fields_remove_button_submit_handler',
      ];
      $element['remove_button']['#ajax']['effect'] = 'fade';
    }
  }
}

/**
 * Submit callback to clear field values for fixed cardinality fields.
 *
 * @param array $form
 *   The complete form structure.
 * @param FormStateInterface $form_state
 *   The current state of the form.
 */
function multiple_fields_remove_button_fixed_submit_handler(array $form, FormStateInterface $form_state) {
  $formValues = $form_state
    ->getValues();
  $formInputs = $form_state
    ->getUserInput();
  $button = $form_state
    ->getTriggeringElement();
  $delta = $button['#delta'];

  // Where in the form we'll find the parent element.
  $address = array_slice($button['#array_parents'], 0, -2);

  // Go one level up in the form, to the widgets container.
  $parent_element = NestedArray::getValue($form, $address);
  $field_name = $parent_element['#field_name'];
  $parents = $parent_element['#field_parents'];
  $field_state = WidgetBase::getWidgetState($parents, $field_name, $form_state);

  // Shift up the following values.
  $cardinality = $parent_element['#cardinality'];
  $i = $delta;
  while ($i < $cardinality - 1) {
    $old_element_address = array_merge($address, [
      $i + 1,
    ]);
    $new_element_address = array_merge($address, [
      $i,
    ]);
    $moving_element = NestedArray::getValue($form, $old_element_address);
    $keys = array_keys($old_element_address, 'widget', TRUE);
    foreach ($keys as $key) {
      unset($old_element_address[$key]);
    }
    $moving_element_value = NestedArray::getValue($formValues, $old_element_address);
    $moving_element_input = NestedArray::getValue($formInputs, $old_element_address);
    $keys = array_keys($new_element_address, 'widget', TRUE);
    foreach ($keys as $key) {
      unset($new_element_address[$key]);
    }

    // Tell the element where it's being moved to.
    $moving_element['#parents'] = $new_element_address;

    // Move the element around.
    NestedArray::setValue($formValues, $moving_element['#parents'], $moving_element_value, TRUE);
    NestedArray::setValue($formInputs, $moving_element['#parents'], $moving_element_input);

    // Save new element values.
    foreach ($formValues as $key => $value) {
      $form_state
        ->setValue($key, $value);
    }
    $form_state
      ->setUserInput($formInputs);
    $i++;
  }

  // Set the last value to be blank.
  $old_element_address = array_merge($address, [
    $cardinality - 1,
  ]);
  $moving_element = NestedArray::getValue($form, $old_element_address);
  NestedArray::setValue($formInputs, $moving_element['#parents'], [
    'target_id' => '',
    '_weight' => $cardinality - 1,
  ]);

  // Re-set the weights.
  // Fix the weights. Field UI lets the weights be in a range of
  // (-1 * item_count) to (item_count). This means that when we remove one,
  // the range shrinks; weights outside of that range then get set to
  // the first item in the select by the browser, floating them to the top.
  // We use a brute force method because we lost weights on both ends
  // and if the user has moved things around, we have to cascade because
  // if I have items weight weights 3 and 4, and I change 4 to 3 but leave
  // the 3, the order of the two 3s now is undefined and may not match what
  // the user had selected.
  $address = array_slice($button['#array_parents'], 0, -2);
  $keys = array_keys($address, 'widget', TRUE);
  foreach ($keys as $key) {
    unset($address[$key]);
  }
  $input = NestedArray::getValue($formInputs, $address);
  if ($input && is_array($input)) {

    // Sort by weight.
    uasort($input, '_field_multiple_value_form_sort_helper');

    // Reweight everything in the correct order.
    $weight = -1 * $field_state['items_count'];
    foreach ($input as $key => $item) {
      if ($item) {
        $input[$key]['_weight'] = $weight++;
      }
    }
    NestedArray::setValue($formInputs, $address, $input);
    $form_state
      ->setUserInput($formInputs);
  }
  $form_state
    ->setRebuild();
}

/**
 * Submit callback to remove an item from the field UI multiple wrapper.
 *
 * When a remove button is submitted, we need to find the item that it
 * referenced and delete it. Since field UI has the deltas as a straight
 * unbroken array key, we have to renumber everything down. Since we do this
 * we *also* need to move all the deltas around in the $form_state['values']
 * and $form_state['input'] so that user changed values follow. This is a bit
 * of a complicated process.
 *
 * @param array $form
 *   The complete form structure.
 * @param FormStateInterface $form_state
 *   The current state of the form.
 */
function multiple_fields_remove_button_submit_handler(array $form, FormStateInterface $form_state) {
  $formValues = $form_state
    ->getValues();
  $formInputs = $form_state
    ->getUserInput();
  $button = $form_state
    ->getTriggeringElement();
  $delta = $button['#delta'];

  // Where in the form we'll find the parent element.
  $address = array_slice($button['#array_parents'], 0, -2);

  // Go one level up in the form, to the widgets container.
  $parent_element = NestedArray::getValue($form, $address);
  $field_name = $parent_element['#field_name'];
  $parents = $parent_element['#field_parents'];
  $field_state = WidgetBase::getWidgetState($parents, $field_name, $form_state);

  // Go ahead and renumber everything from our delta to the last
  // item down one. This will overwrite the item being removed.
  for ($i = $delta; $i <= $field_state['items_count']; $i++) {
    $old_element_address = array_merge($address, [
      $i + 1,
    ]);
    $new_element_address = array_merge($address, [
      $i,
    ]);
    $moving_element = NestedArray::getValue($form, $old_element_address);
    $keys = array_keys($old_element_address, 'widget', TRUE);
    foreach ($keys as $key) {
      unset($old_element_address[$key]);
    }
    $moving_element_value = NestedArray::getValue($formValues, $old_element_address);
    $moving_element_input = NestedArray::getValue($formInputs, $old_element_address);
    $keys = array_keys($new_element_address, 'widget', TRUE);
    foreach ($keys as $key) {
      unset($new_element_address[$key]);
    }

    // Tell the element where it's being moved to.
    $moving_element['#parents'] = $new_element_address;

    // Delete default value for the last deleted element.
    if ($field_state['items_count'] == 0) {
      $struct_key = NestedArray::getValue($formInputs, $new_element_address);
      if (is_null($moving_element_value)) {
        foreach ($struct_key as &$key) {
          $key = '';
        }
        $moving_element_value = $struct_key;
      }
      if (is_null($moving_element_input)) {
        $moving_element_input = $moving_element_value;
      }
    }

    // Move the element around.
    NestedArray::setValue($formValues, $moving_element['#parents'], $moving_element_value, TRUE);
    NestedArray::setValue($formInputs, $moving_element['#parents'], $moving_element_input);

    // Save new element values.
    foreach ($formValues as $key => $value) {
      $form_state
        ->setValue($key, $value);
    }
    $form_state
      ->setUserInput($formInputs);

    // Move the entity in our saved state.
    if (isset($field_state['original_deltas'][$i + 1])) {
      $field_state['original_deltas'][$i] = $field_state['original_deltas'][$i + 1];
    }
    else {
      unset($field_state['original_deltas'][$i]);
    }
  }

  // Replace the deleted entity with an empty one. This helps to ensure that
  // trying to add a new entity won't resurrect a deleted entity
  // from the trash bin.
  // $count = count($field_state['entity']);
  // Then remove the last item. But we must not go negative.
  if ($field_state['items_count'] > 0) {
    $field_state['items_count']--;
  }

  // Fix the weights. Field UI lets the weights be in a range of
  // (-1 * item_count) to (item_count). This means that when we remove one,
  // the range shrinks; weights outside of that range then get set to
  // the first item in the select by the browser, floating them to the top.
  // We use a brute force method because we lost weights on both ends
  // and if the user has moved things around, we have to cascade because
  // if I have items weight weights 3 and 4, and I change 4 to 3 but leave
  // the 3, the order of the two 3s now is undefined and may not match what
  // the user had selected.
  $address = array_slice($button['#array_parents'], 0, -2);
  $keys = array_keys($address, 'widget', TRUE);
  foreach ($keys as $key) {
    unset($address[$key]);
  }
  $input = NestedArray::getValue($formInputs, $address);
  if ($input && is_array($input)) {

    // Sort by weight.
    uasort($input, '_field_multiple_value_form_sort_helper');

    // Reweight everything in the correct order.
    $weight = -1 * $field_state['items_count'];
    foreach ($input as $key => $item) {
      if ($item) {
        $input[$key]['_weight'] = $weight++;
      }
    }
    NestedArray::setValue($formInputs, $address, $input);
    $form_state
      ->setUserInput($formInputs);
  }
  $element_id = isset($form[$field_name]['#id']) ? $form[$field_name]['#id'] : '';
  if (!$element_id) {
    $element_id = $parent_element['#id'];
  }
  $field_state['wrapper_id'] = $element_id;
  WidgetBase::setWidgetState($parents, $field_name, $form_state, $field_state);
  $form_state
    ->setRebuild();
}

/**
 * Implements hook_preprocess_field_multiple_value_form().
 */
function multiple_fields_remove_button_preprocess_field_multiple_value_form(&$variables) {

  // Add wrapper class and stylesheet.
  if (!empty($variables['multiple']) && isset($variables['element']['#cardinality']) && $variables['element']['#cardinality'] != 1) {
    $variables['table']['#attached']['library'][] = 'multiple_fields_remove_button/multiple_fields_remove_button.widget';
    foreach ($variables['table']['#rows'] as &$row) {
      $row['data'][1]['class'][] = 'has-multiple-fields-remove-button';
    }
  }
}

/**
 * Helper function to get element delta.
 *
 * @param array $element
 *   Form element we're operating on.
 *
 * @return int
 *   Element delta.
 */
function _multiple_fields_remove_button_get_delta($element) {
  $delta = 0;
  if (isset($element['#delta'])) {
    $delta = $element['#delta'];
  }
  elseif (isset($element['target_id']['#delta'])) {

    // Handle delta for entity reference field.
    $delta = $element['target_id']['#delta'];
  }
  elseif (isset($element['value']['#delta'])) {

    // Handle delta for integer field.
    $delta = $element['value']['#delta'];
  }
  return $delta;
}

Functions

Namesort descending Description
multiple_fields_remove_button_clear_value_js Ajax callback to empty individual values of limited-cardinality fields.
multiple_fields_remove_button_element_info_alter Implements hook_element_info_alter().
multiple_fields_remove_button_field_widget_form_alter Implements hook_field_widget_form_alter().
multiple_fields_remove_button_fixed_submit_handler Submit callback to clear field values for fixed cardinality fields.
multiple_fields_remove_button_js Ajax callback remove field when remove click is trigger.
multiple_fields_remove_button_preprocess_field_multiple_value_form Implements hook_preprocess_field_multiple_value_form().
multiple_fields_remove_button_process_container Add correct wrapper to the element.
multiple_fields_remove_button_submit_handler Submit callback to remove an item from the field UI multiple wrapper.
_multiple_fields_remove_button_get_delta Helper function to get element delta.