commerce_product_bundles-8.x-1.0/src/Plugin/Field/FieldWidget/CommerceBundleFieldDefaultWidget.php
src/Plugin/Field/FieldWidget/CommerceBundleFieldDefaultWidget.php
<?php namespace Drupal\commerce_product_bundles\Plugin\Field\FieldWidget; use Drupal\commerce_product\Entity\Product; use Drupal\commerce_product\Entity\ProductVariation; use Drupal\Component\Utility\Html; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Ajax\AjaxResponse; use Drupal\Core\Ajax\ReplaceCommand; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldFilteredMarkup; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\Field\WidgetBase; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Validator\ConstraintViolationInterface; /** * Plugin implementation of the 'product_bundle_field_default' widget. * * @FieldWidget( * id = "product_bundle_field_default", * label = @Translation("Commerce product bundle field widget"), * field_types = { * "product_bundle_field" * }, * ) */ class CommerceBundleFieldDefaultWidget extends WidgetBase implements ContainerFactoryPluginInterface { /** * The entity type manager. * * @var \Drupal\Core\Entity\EntityTypeManagerInterface */ protected $entityTypeManager; /** * CommerceBundleFieldDefaultWidget constructor. * * @param $plugin_id * @param $plugin_definition * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition * @param array $settings * @param array $third_party_settings * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */ public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityTypeManagerInterface $entity_type_manager) { parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings); $this->entityTypeManager = $entity_type_manager; } /** * {@inheritdoc} */ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { return new static( $plugin_id, $plugin_definition, $configuration['field_definition'], $configuration['settings'], $configuration['third_party_settings'], $container->get('entity_type.manager') ); } /** * {@inheritdoc} */ public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { $item = $items[$delta]; $values = $item->getValue(); $element += [ '#type' => 'details', '#collapsible' => TRUE, '#open' => FALSE ]; $products = isset($values['product_id']) ? $values['product_id'] : []; $element['product_id'] = [ '#type' => 'select2', '#target_type' => 'commerce_product', '#title' => $this->t('Products'), '#default_value' => $products, '#multiple' => FALSE, '#autocomplete' => TRUE, '#ajax' => [ 'callback' => [get_class($this), 'ajaxGetProductVariations'], 'event' => 'change', 'wrapper' => 'product-variation--wrapper-' . $delta, 'progress' => [ 'type' => 'throbber', 'message' => $this->t('Verifying entry...'), ], ], ]; $default_product = $products; if ($form_state->isRebuilding()) { $parents = array_merge($element['#field_parents'], [$items->getName(), $delta, 'product_id']); $selected_product_id = (array) NestedArray::getValue($form_state->getUserInput(), $parents); if(!empty($selected_product_id)){ $default_product = reset($selected_product_id); } } // Get default options for product variations select. $product_variations = $this->entityTypeManager->getStorage('commerce_product_variation')->getQuery() ->accessCheck(FALSE) ->condition('default_langcode', 1); // If we have default product selected limit options. if (!empty($default_product)){ $product_variations->condition('product_id', $default_product); } $variations = $product_variations->execute(); $default_options = self::getVariationOptions($variations); // Get default variations. // Construct prefix: if we have default value show field, if not hide it. $variations_default = isset($values['variation_ids']) ? $values['variation_ids'] : []; $prefix = '<div id="product-variation--wrapper-' . $delta . '" class="hidden">'; if(!empty($variations_default) || !empty($default_product)){ $prefix = '<div id="product-variation--wrapper-' . $delta . '">'; } $element['variation_ids'] = [ '#type' => 'select2', '#options' => $default_options, '#title' => $this->t('Product Variations'), '#default_value' => $variations_default, '#multiple' => TRUE, '#prefix' => $prefix, '#suffix' => '</div>', '#ajax' => [ 'callback' => [get_class($this), 'ajaxSetProductVariations'], 'event' => 'change', 'wrapper' => 'product-variation--wrapper', // This element is updated with this AJAX callback. ], ]; // Get default quantity, defaults to 1. $quantity = isset($items[$delta]->quantity) ? $items[$delta]->quantity : 1; $element['quantity'] = [ '#type' => 'number', '#title' => t('Product quantity'), '#default_value' => $quantity, '#min' => 1, '#required' => TRUE, ]; return $element; } /** * Helper function for getting variation options for select 2 element. * * @param $variations * * @return array */ private static function getVariationOptions($variations){ $options = []; foreach ($variations as $variation_id){ $product_variation_eck = ProductVariation::load($variation_id); $options[$variation_id] = $product_variation_eck->label(); } return $options; } /** * Ajax callback. * * Shows product variations field based on selected product. */ public static function ajaxGetProductVariations(array $form, FormStateInterface $form_state) { $triggering_el = $form_state->getTriggeringElement(); $parents = array_slice($triggering_el['#array_parents'], 0, -1); $element = NestedArray::getValue($form, $parents); $wrapper_id = $triggering_el['#ajax']['wrapper']; $delta = $element['#delta']; $selected_value = $form_state->getValue($parents[0]); // Get selected product and load it. $product_id = $selected_value[$delta]['product_id']; // Check if selected product is an array. if (is_array($product_id)) { foreach ($product_id as $key => $value) { $product_id = $value['target_id']; } } $product = Product::load($product_id); // Get product variations to set them as options. $variations = $product->getVariationIds(); // Get product variation options. $options = self::getVariationOptions($variations); // Set available options. $element['variation_ids']['#options'] = $options; $element['variation_ids']['#prefix'] = '<div id="product-variation--wrapper-' . $delta . '" class="show">'; // Reset default value on product change. $element['variation_ids']['#default_value'] = []; $response = new AjaxResponse(); $response->addCommand(new ReplaceCommand("#$wrapper_id", $element['variation_ids'])); return $response; } /** * Ajax callback. * * Dummy callback so we do trigger field save on variations change. */ public static function ajaxSetProductVariations(array $form, FormStateInterface $form_state) { return new AjaxResponse(); } /** * {@inheritdoc} */ public function errorElement(array $element, ConstraintViolationInterface $error, array $form, FormStateInterface $form_state) { return $element['variation_ids']; } /** * {@inheritdoc} */ public function massageFormValues(array $values, array $form, FormStateInterface $form_state) { foreach ($values as $key => $value) { if (is_array($value['variation_ids'])) { $values[$key]['variation_ids'] = $value['variation_ids']; } if (isset($value['product_id'])) { $values[$key]['product_id'] = $value['product_id']; // Check if selected product is an array. if (is_array($value['product_id'])) { foreach ($value['product_id'] as $product_key => $product_value) { $values[$key]['product_id'] = $product_value['target_id']; } } } } return $values; } /** * Special handling to create form elements for multiple values. * * Code reused from https://www.drupal.org/project/drupal/issues/1038316 * @author - patch: https://www.drupal.org/files/issues/2019-02-07/1038316-188.patch (Thanks!) */ protected function formMultipleElements(FieldItemListInterface $items, array &$form, FormStateInterface $form_state) { $field_name = $this->fieldDefinition->getName(); $cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality(); $parents = $form['#parents']; $field_state = static::getWidgetState($parents, $field_name, $form_state); // Determine the number of widgets to display. switch ($cardinality) { case FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED: $max = $field_state['items_count'] - 1; $is_multiple = TRUE; break; default: $max = $cardinality - 1; $is_multiple = ($cardinality > 1); break; } $title = $this->fieldDefinition->getLabel(); $description = FieldFilteredMarkup::create(\Drupal::token()->replace($this->fieldDefinition->getDescription())); $id_prefix = implode('-', array_merge($parents, [$field_name])); $wrapper_id = Html::cleanCssIdentifier($id_prefix . '-add-wrapper'); $elements = []; for ($delta = 0; $delta <= $max; $delta++) { // Add a new empty item if it doesn't exist yet at this delta. if (!isset($items[$delta])) { $items->appendItem(); } // For multiple fields, title and description are handled by the wrapping // table. if ($is_multiple) { $element = [ '#title' => $this->t('@title (value @number)', ['@title' => $title, '@number' => $delta + 1]), '#title_display' => 'invisible', '#description' => '', ]; } else { $element = [ '#title' => $title, '#title_display' => 'before', '#description' => $description, ]; } $element = $this->formSingleElement($items, $delta, $element, $form, $form_state); if ($element) { // Input field for the delta (drag-n-drop reordering). if ($is_multiple) { // Set custom weight handling - use value from user input. $input = $form_state->getUserInput(); $weight = isset($input[$field_name][$delta]['_weight']) ? $input[$field_name][$delta]['_weight'] : $delta; // We name the element '_weight' to avoid clashing with elements // defined by widget. $element['_weight'] = [ '#type' => 'weight', '#title' => $this->t('Weight for row @number', ['@number' => $delta + 1]), '#title_display' => 'invisible', '#delta' => $max, '#value' => $weight, '#weight' => 100, ]; $element['actions'] = [ '#type' => 'actions', 'remove_button' => [ '#delta' => $delta, '#name' => implode('_', $element['#field_parents']) . "_remove_button_$delta", '#type' => 'submit', '#value' => t('Remove'), '#validate' => [], '#submit' => [[static::class, 'submitRemove']], '#limit_validation_errors' => [], '#attributes' => [ 'class' => ['remove-field-delta--' . $delta], ], '#ajax' => [ 'callback' => [static::class, 'removeAjaxContentRefresh'], 'wrapper' => $wrapper_id, 'effect' => 'fade', ], ], ]; } $elements[$delta] = $element; } } $elements += [ '#theme' => 'field_multiple_value_form', '#field_name' => $field_name, '#cardinality' => $cardinality, '#cardinality_multiple' => $this->fieldDefinition->getFieldStorageDefinition()->isMultiple(), '#required' => $this->fieldDefinition->isRequired(), '#title' => $title, '#description' => $description, '#max_delta' => $max, ]; // Add 'add more' button, if not working with a programmed form. if ($cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED && !$form_state->isProgrammed()) { $form['#wrapper_id'] = $wrapper_id; $elements['#prefix'] = '<div id="' . $wrapper_id . '">'; $elements['#suffix'] = '</div>'; $elements['add_more'] = [ '#type' => 'submit', '#name' => strtr($id_prefix, '-', '_') . '_add_more', '#value' => $delta > 0 ? t('Add another item') : t('Add item'), '#attributes' => ['class' => ['field-add-more-submit']], '#limit_validation_errors' => [], '#submit' => [[static::class, 'addMoreSubmit']], '#ajax' => [ 'callback' => [static::class, 'addMoreAjax'], 'wrapper' => $wrapper_id, 'effect' => 'fade', ], ]; } return $elements; } /** * Ajax submit callback for the "Remove" button. * * This re-numbers form elements and removes an item. * * Code reused from https://www.drupal.org/project/drupal/issues/1038316 * @author - patch: https://www.drupal.org/files/issues/2019-02-07/1038316-188.patch (Thanks!) */ public static function submitRemove(&$form, FormStateInterface $form_state) { $button = $form_state->getTriggeringElement(); $delta = $button['#delta']; $array_parents = array_slice($button['#array_parents'], 0, -4); $old_parents = array_slice($button['#parents'], 0, -3); $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); for ($i = $delta; $i < $field_state['items_count']; $i++) { $old_element_widget_parents = array_merge($array_parents, ['widget', $i + 1]); $old_element_parents = array_merge($old_parents, [$i + 1]); $new_element_parents = array_merge($old_parents, [$i]); $moving_element = NestedArray::getValue($form, $old_element_widget_parents); $moving_element_input = NestedArray::getValue($form_state->getUserInput(), $old_element_parents); // Tell the element where it's being moved to. $moving_element['#parents'] = $new_element_parents; // Move the element around. $user_input = $form_state->getUserInput(); NestedArray::setValue($user_input, $moving_element['#parents'], $moving_element_input); $user_input[$field_name] = array_filter(NestedArray::getValue($user_input, $old_parents)); $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']--; } $key_exists = ''; unset($array_parents[1]); $input = NestedArray::getValue($form_state->getUserInput(), $array_parents, $key_exists); $weight = -1 * $field_state['items_count']; foreach ($input as $key => $item) { if ($item) { $input[$key]['_weight'] = $weight++; } } $user_input = $form_state->getUserInput(); NestedArray::setValue($user_input, $array_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 page content to replace the page content made obsolete * by the form submission. * * Code reused from https://www.drupal.org/project/drupal/issues/1038316 * @author - patch: https://www.drupal.org/files/issues/2019-02-07/1038316-188.patch (Thanks!) */ public static function removeAjaxContentRefresh(array &$form, FormStateInterface $form_state) { $button = $form_state->getTriggeringElement(); return NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -3)); } }