diff --git a/modules/cart/config/install/core.entity_view_mode.commerce_product_attribute_value.add_to_cart.yml b/modules/cart/config/install/core.entity_view_mode.commerce_product_attribute_value.add_to_cart.yml new file mode 100644 index 0000000..cbabe93 --- /dev/null +++ b/modules/cart/config/install/core.entity_view_mode.commerce_product_attribute_value.add_to_cart.yml @@ -0,0 +1,9 @@ +langcode: en +status: true +dependencies: + module: + - commerce_product +id: commerce_product_attribute_value.add_to_cart +label: 'Add to Cart Form' +targetEntityType: commerce_product_attribute_value +cache: true diff --git a/modules/checkout/src/Annotation/CommerceCheckoutPane.php b/modules/checkout/src/Annotation/CommerceCheckoutPane.php index 5449b32..ce2ce42 100755 --- a/modules/checkout/src/Annotation/CommerceCheckoutPane.php +++ b/modules/checkout/src/Annotation/CommerceCheckoutPane.php @@ -23,8 +23,6 @@ class CommerceCheckoutPane extends Plugin { /** * The human-readable name of the plugin. * - * Shown as the title of the pane form if the wrapper_element is 'fieldset'. - * * @ingroup plugin_translatable * * @var \Drupal\Core\Annotation\Translation @@ -40,13 +38,4 @@ class CommerceCheckoutPane extends Plugin { */ public $default_step; - /** - * The wrapper element to use when rendering the pane's form. - * - * E.g: 'container', 'fieldset'. Defaults to 'container'. - * - * @var string - */ - public $wrapper_element; - } diff --git a/modules/checkout/src/CheckoutPaneManager.php b/modules/checkout/src/CheckoutPaneManager.php index 25ef2fc..0adf773 100755 --- a/modules/checkout/src/CheckoutPaneManager.php +++ b/modules/checkout/src/CheckoutPaneManager.php @@ -23,7 +23,6 @@ class CheckoutPaneManager extends DefaultPluginManager { 'id' => '', 'label' => '', 'default_step' => '_disabled', - 'wrapper_element' => 'container', ]; /** diff --git a/modules/checkout/src/Plugin/Commerce/CheckoutFlow/CheckoutFlowWithPanesBase.php b/modules/checkout/src/Plugin/Commerce/CheckoutFlow/CheckoutFlowWithPanesBase.php index 6acb5f5..757b9c0 100755 --- a/modules/checkout/src/Plugin/Commerce/CheckoutFlow/CheckoutFlowWithPanesBase.php +++ b/modules/checkout/src/Plugin/Commerce/CheckoutFlow/CheckoutFlowWithPanesBase.php @@ -500,9 +500,8 @@ abstract class CheckoutFlowWithPanesBase extends CheckoutFlowBase implements Che $panes = $this->getPanes($this->stepId); foreach ($panes as $pane_id => $pane) { $form[$pane_id] = [ + '#type' => 'container', '#parents' => [$pane_id], - '#type' => $pane->getWrapperElement(), - '#title' => $pane->getLabel(), '#access' => $pane->isVisible(), ]; $form[$pane_id] = $pane->buildPaneForm($form[$pane_id], $form_state); diff --git a/modules/checkout/src/Plugin/Commerce/CheckoutPane/BillingInformation.php b/modules/checkout/src/Plugin/Commerce/CheckoutPane/BillingInformation.php index cd48025..21bae8c 100755 --- a/modules/checkout/src/Plugin/Commerce/CheckoutPane/BillingInformation.php +++ b/modules/checkout/src/Plugin/Commerce/CheckoutPane/BillingInformation.php @@ -17,7 +17,6 @@ use Symfony\Component\DependencyInjection\ContainerInterface; * id = "billing_information", * label = "Billing information", * default_step = "order_information", - * wrapper_element = "fieldset", * ) */ class BillingInformation extends CheckoutPaneBase implements CheckoutPaneInterface, ContainerFactoryPluginInterface { diff --git a/modules/checkout/src/Plugin/Commerce/CheckoutPane/CheckoutPaneBase.php b/modules/checkout/src/Plugin/Commerce/CheckoutPane/CheckoutPaneBase.php index 767d598..263dd5d 100755 --- a/modules/checkout/src/Plugin/Commerce/CheckoutPane/CheckoutPaneBase.php +++ b/modules/checkout/src/Plugin/Commerce/CheckoutPane/CheckoutPaneBase.php @@ -123,13 +123,6 @@ abstract class CheckoutPaneBase extends PluginBase implements CheckoutPaneInterf /** * {@inheritdoc} */ - public function getWrapperElement() { - return $this->pluginDefinition['wrapper_element']; - } - - /** - * {@inheritdoc} - */ public function getStepId() { return $this->configuration['step']; } diff --git a/modules/checkout/src/Plugin/Commerce/CheckoutPane/CheckoutPaneInterface.php b/modules/checkout/src/Plugin/Commerce/CheckoutPane/CheckoutPaneInterface.php index ac75861..8f1a1a0 100755 --- a/modules/checkout/src/Plugin/Commerce/CheckoutPane/CheckoutPaneInterface.php +++ b/modules/checkout/src/Plugin/Commerce/CheckoutPane/CheckoutPaneInterface.php @@ -33,17 +33,6 @@ interface CheckoutPaneInterface extends ConfigurablePluginInterface, PluginFormI public function getLabel(); /** - * Gets the pane wrapper element. - * - * Used when rendering the pane's form. - * E.g: 'container', 'fieldset'. Defaults to 'container'. - * - * @return string - * The pane wrapper element. - */ - public function getWrapperElement(); - - /** * Gets the pane step ID. * * @return string diff --git a/modules/checkout/src/Plugin/Commerce/CheckoutPane/ContactInformation.php b/modules/checkout/src/Plugin/Commerce/CheckoutPane/ContactInformation.php index e8cbeb3..bc39c6d 100755 --- a/modules/checkout/src/Plugin/Commerce/CheckoutPane/ContactInformation.php +++ b/modules/checkout/src/Plugin/Commerce/CheckoutPane/ContactInformation.php @@ -15,7 +15,6 @@ use Symfony\Component\DependencyInjection\ContainerInterface; * id = "contact_information", * label = "Contact information", * default_step = "order_information", - * wrapper_element = "fieldset", * ) */ class ContactInformation extends CheckoutPaneBase implements CheckoutPaneInterface, ContainerFactoryPluginInterface { diff --git a/modules/product/commerce_product.libraries.yml b/modules/product/commerce_product.libraries.yml index bb2a1f2..5cf9713 100644 --- a/modules/product/commerce_product.libraries.yml +++ b/modules/product/commerce_product.libraries.yml @@ -3,3 +3,8 @@ form: css: theme: css/product.form.css: {} +rendered-attributes: + version: VERSION + css: + theme: + css/product.rendered-attributes.css: {} diff --git a/modules/product/config/schema/commerce_product.schema.yml b/modules/product/config/schema/commerce_product.schema.yml index 3e39454..914bcfd 100644 --- a/modules/product/config/schema/commerce_product.schema.yml +++ b/modules/product/config/schema/commerce_product.schema.yml @@ -42,6 +42,9 @@ commerce_product.commerce_product_attribute.*: label: type: label label: 'Label' + elementType: + type: string + label: 'Element type' field.formatter.settings.commerce_add_to_cart: type: mapping diff --git a/modules/product/css/product.rendered-attributes.css b/modules/product/css/product.rendered-attributes.css new file mode 100644 index 0000000..c7046d3 --- /dev/null +++ b/modules/product/css/product.rendered-attributes.css @@ -0,0 +1,14 @@ +.product--rendered-attribute .form-item { + display: inline-block; + vertical-align: middle; + margin: 2px; +} +.product--rendered-attribute label.option { + display: inline-block; +} +.product--rendered-attribute .form-radio { + display: none; +} +.product--rendered-attribute__selected ~ label.option { + border: 1px solid; +} diff --git a/modules/product/src/Element/CommerceProductRenderedAttribute.php b/modules/product/src/Element/CommerceProductRenderedAttribute.php new file mode 100644 index 0000000..8a76bc2 --- /dev/null +++ b/modules/product/src/Element/CommerceProductRenderedAttribute.php @@ -0,0 +1,78 @@ + 'commerce_product_rendered_attribute', + * '#title' => $this->t('Poll status'), + * '#default_value' => 1, + * '#options' => array(0 => $this->t('Closed'), 1 => $this->t('Active')), + * ); + * @endcode + * + * @FormElement("commerce_product_rendered_attribute") + */ +class CommerceProductRenderedAttribute extends Radios { + + /** + * Expands a radios element into individual radio elements. + */ + public static function processRadios(&$element, FormStateInterface $form_state, &$complete_form) { + if (count($element['#options']) > 0) { + + $element['#attached']['library'][] = 'commerce_product/rendered-attributes'; + + /** @var \Drupal\Core\Entity\EntityStorageInterface $storage */ + $storage = \Drupal::service('entity_type.manager')->getStorage('commerce_product_attribute_value'); + $attribute_values = $storage->loadMultiple(array_keys($element['#options'])); + + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = \Drupal::service('renderer'); + + $weight = 0; + foreach ($element['#options'] as $key => $choice) { + $rendered_attribute = \Drupal::entityTypeManager()->getViewBuilder('commerce_product_attribute_value')->view($attribute_values[$key], 'add_to_cart'); + $attributes = $element['#attributes']; + + if (isset($element['#default_value']) && $element['#default_value'] == $key) { + $attributes['class'][] = 'product--rendered-attribute__selected'; + } + // Maintain order of options as defined in #options, in case the element + // defines custom option sub-elements, but does not define all option + // sub-elements. + $weight += 0.001; + + $element += [$key => []]; + // Generate the parents as the autogenerator does, so we will have a + // unique id for each radio button. + $parents_for_id = array_merge($element['#parents'], [$key]); + $element[$key] += [ + '#type' => 'radio', + '#title' => $renderer->render($rendered_attribute), + '#return_value' => $key, + '#default_value' => isset($element['#default_value']) ? $element['#default_value'] : FALSE, + '#attributes' => $attributes, + '#parents' => $element['#parents'], + '#id' => HtmlUtility::getUniqueId('edit-' . implode('-', $parents_for_id)), + '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL, + // Errors should only be shown on the parent radios element. + '#error_no_message' => TRUE, + '#weight' => $weight, + ]; + } + } + + $element['#attributes']['class'][] = 'product--rendered-attribute'; + + return $element; + } +} diff --git a/modules/product/src/Entity/ProductAttribute.php b/modules/product/src/Entity/ProductAttribute.php index 5eccbe6..97958e2 100644 --- a/modules/product/src/Entity/ProductAttribute.php +++ b/modules/product/src/Entity/ProductAttribute.php @@ -32,7 +32,8 @@ use Drupal\Core\Entity\EntityStorageInterface; * }, * config_export = { * "id", - * "label" + * "label", + * "elementType" * }, * links = { * "add-form" = "/admin/commerce/product-attributes/add", @@ -60,6 +61,13 @@ class ProductAttribute extends ConfigEntityBundleBase implements ProductAttribut protected $label; /** + * The attribute element type. + * + * @var string + */ + protected $elementType; + + /** * {@inheritdoc} */ public function getValues() { @@ -70,6 +78,13 @@ class ProductAttribute extends ConfigEntityBundleBase implements ProductAttribut /** * {@inheritdoc} */ + public function getElementType() { + return (!$this->elementType) ? 'select' : $this->elementType; + } + + /** + * {@inheritdoc} + */ public static function postDelete(EntityStorageInterface $storage, array $entities) { /** @var \Drupal\commerce_product\Entity\ProductAttributeInterface[] $entities */ parent::postDelete($storage, $entities); diff --git a/modules/product/src/Entity/ProductAttributeInterface.php b/modules/product/src/Entity/ProductAttributeInterface.php index 2db61b1..c7110b0 100644 --- a/modules/product/src/Entity/ProductAttributeInterface.php +++ b/modules/product/src/Entity/ProductAttributeInterface.php @@ -16,4 +16,10 @@ interface ProductAttributeInterface extends ConfigEntityInterface { */ public function getValues(); + /** + * Gets the attribute element type. + * + * @return string + */ + public function getElementType(); } diff --git a/modules/product/src/Entity/ProductAttributeValue.php b/modules/product/src/Entity/ProductAttributeValue.php index c33d112..b97c7ee 100644 --- a/modules/product/src/Entity/ProductAttributeValue.php +++ b/modules/product/src/Entity/ProductAttributeValue.php @@ -119,6 +119,7 @@ class ProductAttributeValue extends ContentEntityBase implements ProductAttribut 'type' => 'string_textfield', 'weight' => -5, ]) + ->setDisplayConfigurable('view', TRUE) ->setDisplayConfigurable('form', TRUE); $fields['weight'] = BaseFieldDefinition::create('integer') diff --git a/modules/product/src/Form/ProductAttributeForm.php b/modules/product/src/Form/ProductAttributeForm.php index 2da03f1..37191e2 100644 --- a/modules/product/src/Form/ProductAttributeForm.php +++ b/modules/product/src/Form/ProductAttributeForm.php @@ -3,13 +3,44 @@ namespace Drupal\commerce_product\Form; use Drupal\Core\Entity\BundleEntityFormBase; +use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\Form\FormStateInterface; +use Drupal\field\Entity\FieldConfig; +use Drupal\field\FieldConfigInterface; use Drupal\language\Entity\ContentLanguageSettings; +use Symfony\Component\DependencyInjection\ContainerInterface; class ProductAttributeForm extends BundleEntityFormBase { /** + * The entity field manager. + * + * @var \Drupal\Core\Entity\EntityFieldManagerInterface + */ + protected $entityFieldManager; + + /** + * Constructs a new ProductAttributeForm object. + * + * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager + * The entity field manager. + */ + public function __construct(EntityFieldManagerInterface $entity_field_manager) { + $this->entityFieldManager = $entity_field_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_field.manager') + ); + } + + /** * {@inheritdoc} */ public function form(array $form, FormStateInterface $form_state) { @@ -32,6 +63,16 @@ class ProductAttributeForm extends BundleEntityFormBase { ], '#maxlength' => EntityTypeInterface::BUNDLE_MAX_LENGTH, ]; + $form['elementType'] = [ + '#type' => 'select', + '#default_value' => $attribute->getElementType(), + '#options' => [ + 'radios' => $this->t('Radio buttons'), + 'select' => $this->t('Select list'), + 'commerce_product_rendered_attribute' => $this->t('Rendered attribute'), + ], + '#description' => $this->t('Controls how the attribute is displayed on the add to cart form.'), + ]; if ($this->moduleHandler->moduleExists('language')) { $form['language'] = [ diff --git a/modules/product/src/Plugin/Field/FieldWidget/ProductVariationAttributesWidget.php b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationAttributesWidget.php index 0075385..7242a82 100644 --- a/modules/product/src/Plugin/Field/FieldWidget/ProductVariationAttributesWidget.php +++ b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationAttributesWidget.php @@ -45,6 +45,13 @@ class ProductVariationAttributesWidget extends WidgetBase implements ContainerFa protected $variationStorage; /** + * The product attribute storage. + * + * @var \Drupal\Core\Entity\EntityStorageInterface + */ + protected $attributeStorage; + + /** * Constructs a new ProductVariationAttributesWidget object. * * @param string $plugin_id @@ -67,6 +74,7 @@ class ProductVariationAttributesWidget extends WidgetBase implements ContainerFa $this->attributeFieldManager = $attribute_field_manager; $this->variationStorage = $entity_type_manager->getStorage('commerce_product_variation'); + $this->attributeStorage = $entity_type_manager->getStorage('commerce_product_attribute'); } /** @@ -198,7 +206,7 @@ class ProductVariationAttributesWidget extends WidgetBase implements ContainerFa ]; foreach ($this->getAttributeInfo($selected_variation, $variations) as $field_name => $attribute) { $element['attributes'][$field_name] = [ - '#type' => $this->getSetting('attribute_widget_type'), + '#type' => $attribute['element_type'], '#title' => $attribute['title'], '#options' => $attribute['values'], '#required' => $attribute['required'], @@ -290,13 +298,17 @@ class ProductVariationAttributesWidget extends WidgetBase implements ContainerFa $field_definitions = $this->attributeFieldManager->getFieldDefinitions($selected_variation->bundle()); $field_map = $this->attributeFieldManager->getFieldMap($selected_variation->bundle()); $field_names = array_column($field_map, 'field_name'); + $index = 0; - foreach ($field_names as $field_name) { + foreach ($field_map as $field_name => $map) { + /** @var \Drupal\commerce_product\Entity\ProductAttributeInterface $attribute_type */ + $attribute_type = $this->attributeStorage->load($map['attribute_id']); $field = $field_definitions[$field_name]; $attributes[$field_name] = [ 'field_name' => $field_name, 'title' => $field->getLabel(), 'required' => $field->isRequired(), + 'element_type' => $attribute_type->getElementType(), ]; // The first attribute gets all values. Every next attribute gets only // the values from variations matching the previous attribute value. diff --git a/modules/product/src/ProductAttributeFieldManager.php b/modules/product/src/ProductAttributeFieldManager.php index 694d27c..30db44d 100644 --- a/modules/product/src/ProductAttributeFieldManager.php +++ b/modules/product/src/ProductAttributeFieldManager.php @@ -143,7 +143,7 @@ class ProductAttributeFieldManager implements ProductAttributeFieldManagerInterf $handler_settings = $definition->getSetting('handler_settings'); $component = $form_display->getComponent($field_name); - $field_map[$bundle][] = [ + $field_map[$bundle][$field_name] = [ 'attribute_id' => reset($handler_settings['target_bundles']), 'field_name' => $field_name, 'weight' => $component ? $component['weight'] : 0,