commerce_checkout_accordion-1.0.x-dev/src/Form/CheckoutFlowAccordion.php

src/Form/CheckoutFlowAccordion.php
<?php

namespace Drupal\commerce_checkout_accordion\Form;

use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Ajax\InvokeCommand;
use Drupal\commerce\AjaxFormTrait;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;

/**
 * Implements accordion checkout flow.
 *
 * @package Drupal\commerce_checkout_accordion\Form
 */
class CheckoutFlowAccordion {

  use StringTranslationTrait;
  use DependencySerializationTrait;

  /**
   * The checkout panes array.
   *
   * The array consists of a generated index only useful within this class and
   * the name of the checkout pane as value.
   *
   * @var array
   */
  protected $checkoutPanes;

  /**
   * Add a button to each checkout pane.
   *
   * @param array $form
   *   An associative array containing the structure of the form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   * @param string $form_id
   *   The form id.
   *
   * @see hook_form_alter()
   */
  public function formAlter(array &$form, FormStateInterface $form_state, $form_id) {
    if ($form_id === 'commerce_checkout_flow_multistep_default' && $form["#step_id"] === "order_information") {
      $this->checkoutPanes = CheckoutFlowAccordion::getCheckoutPanesNames($form);
      // Find the current pane index.
      $current_pane_pos = $this->calcCurrentPanePos($form_state);

      $total_panes = count($this->checkoutPanes);

      /** @var \Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowWithPanesInterface $checkout_flow */
      $checkout_flow = $form_state->getFormObject();

      foreach ($this->checkoutPanes as $index => $pane_name) {
        // Only interested in panes that are like a group.
        if ($form[$pane_name]['#type'] !== 'fieldset' && $form[$pane_name]['#type'] !== 'details') {
          continue;
        }

        // Change each pane form element type to "details".
        $form[$pane_name]['#type'] = 'details';

        // Add the next button to all panes except the last one.
        // Even though we really need the next button for the current/next pane,
        // adding the next button to all panes is more robust in case of errors.
        if ($index != $total_panes - 1) {
          $next_pane_name = (string) $form[$this->checkoutPanes[$index + 1]]['#title'];
          $form[$pane_name]['next'] = [
            '#type' => 'button',
            '#value' => "Next: $next_pane_name",
            '#name' => 'next',
            '#ajax' => [
              'callback' => [$this, 'registerCallback'],
            ],
            '#limit_validation_errors' => [$form[$pane_name]['#parents']],
            '#attributes' => [
              'class' => [
                'checkout-next-step',
              ],
            ],
            '#prefix' => '<div class="checkout-pane__next-step">',
            '#suffix' => '</div>',
          ];
        }

        // The past panes.
        if ($index < $current_pane_pos) {
          // Show summary.
          $paneObj = $checkout_flow->getPane($pane_name);
          if ($summary = $paneObj->buildPaneSummary()) {
            // BC layer for panes which still return rendered strings.
            if ($summary && !is_array($summary)) {
              $summary = [
                '#markup' => $summary,
              ];
            }
            $form[$pane_name]['summary'] = $summary;
          }

          // Add an Edit button.
          $form[$pane_name]['edit'] = [
            '#value' => $this->t("Edit"),
            // #value is the same, so #name must be unqiue for Form API.
            '#name' => $this->checkoutPanes[$index] . '_edit',
            '#type' => 'button',
            '#ajax' => [
              'callback' => [$this, 'registerCallback'],
            ],
            // Edit button should surpress all validation errors.
            '#limit_validation_errors' => [],
            '#prefix' => "<span> (",
            '#suffix' => ')</span>',
            '#attributes' => [
              'class' => [
                'link',
              ],
            ],
          ];
        }
        // Disable all details panes except the current one.
        if ($index != $current_pane_pos) {
          $form[$pane_name]['#attributes']['onclick'] = 'return false';
        }
      }

      // Open the current pane.
      $current_pane_name = $this->checkoutPanes[$current_pane_pos];
      $form[$current_pane_name]['#attributes']['class'][] = 'collapsible';
      $form[$current_pane_name]['#open'] = 'open';

      // Attach the JS file.
      $form["#attached"]["library"][] = 'commerce_checkout_accordion/panes_accordion';
    }
  }

  /**
   * Get the checkout panes.
   *
   * This won't be needed after
   * https://www.drupal.org/project/commerce/issues/3201864
   *
   * @return array
   *   The array of checkout panes' names.
   */
  public static function getCheckoutPanesNames(array &$form) {
    $pane_names = [];
    foreach ($form as $key => $leaf) {
      if (is_array($leaf) && isset($leaf['#pane_id'])) {
        $pane_names[] = $key;
      }
    }

    return $pane_names;
  }

  /**
   * Refresh the checkout pane or animate the accordion to the next one.
   *
   * This callback will occur *after* the form has been rebuilt by buildForm().
   * Since that's the case, the $form array should contain the right values for
   * the instrument type field that reflect the current value of the instrument
   * family field.
   *
   * @param array $form
   *   An associative array containing the structure of the form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   *
   * @return array
   *   The portion of the render structure that will replace the form element.
   */
  public function registerCallback(array $form, FormStateInterface $form_state) {
    $response = AjaxFormTrait::ajaxRefreshForm($form, $form_state);

    // Add custom commands to open the next accordion item.
    if (!$form_state->hasAnyErrors()) {
      // currentPanePos is actually from the form built containting the
      // triggering element, not the form just built. See docs
      // https://api.drupal.org/api/drupal/core%21core.api.php/group/ajax/9.4.x#sub_callback
      $current_pane_pos = $this->calcCurrentPanePos($form_state);
      $triggering_pane = $form[$this->checkoutPanes[$current_pane_pos]];
      $id = $triggering_pane["#attributes"]["data-drupal-selector"];
      // The current checkout pane.
      $selector = "[data-drupal-selector=\"$id\"]";
      $method = 'attr';
      $arguments = ['open', ''];
      $response->addCommand(new InvokeCommand($selector, $method, $arguments));
      $arguments = ['onclick', ''];
      $response->addCommand(new InvokeCommand($selector, $method, $arguments));
    }

    return $response;
  }

  /**
   * Calculate the current pane index.
   *
   * We cannot store the current pane as a member variable in the class, which
   * will be cached between requests.
   *
   * The current checkout is the only one with accordion itme item opened. It is
   * calculated from a triggering form element (@todo URL bookmark). If there is
   * no triggering element, then the first pane should be the current one. The
   * panes before the current one should have an edit link, while the ones after
   * should all be locked.
   *
   * @return int
   *   The current pane index.
   */
  protected function calcCurrentPanePos(FormStateInterface $form_state) {
    $current_pane_pos = 0;

    $triggering_element = $form_state->getTriggeringElement();
    $triggering_pane_name = empty($triggering_element) ? NULL : $triggering_element['#parents'][0];
    // Find the current pane index.
    if ($triggering_element) {
      $triggering_pos = array_search($triggering_pane_name, $this->checkoutPanes);
      // If there is no form rebuild, it means the customer moves on to the
      // the next pane. If the edit button is clicked, then the customer moves
      // backward. If the ajax is triggered by other element inside the pane,
      // such as coupon, shipping or switching a payment method, the customer
      // stays at the current pane.
      $current_pane_pos = !$form_state->hasAnyErrors()
        && $triggering_element['#name'] == 'next'
        ? $triggering_pos + 1 : $triggering_pos;
    }

    return $current_pane_pos;
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc