stacks-8.x-1.x-dev/src/WidgetAdmin/Step/StepOne.php

src/WidgetAdmin/Step/StepOne.php
<?php

namespace Drupal\stacks\WidgetAdmin\Step;

use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Link;
use Drupal\Core\Url;
use Drupal\stacks\Entity\WidgetEntityType;
use Drupal\stacks\Entity\WidgetInstanceEntity;
use Drupal\stacks\Widget\WidgetTemplates;
use Drupal\stacks\WidgetAdmin\Button\StepOneNextButton;
use Drupal\stacks\WidgetAdmin\Button\StepExistingFinishButton;
use Drupal\stacks\WidgetAdmin\Validator\ValidatorRequired;
use Drupal\stacks\WidgetAdmin\Validator\ValidatorCustom;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Element\Tableselect;
use Drupal\Core\StringTranslation\TranslatableMarkup;

/**
 * Class StepOne.
 * @package Drupal\stacks\WidgetAdmin\Step
 */
class StepOne extends BaseStep {

  /**
   * @inheritDoc.
   */
  public function setStep() {
    return StepsEnum::STEP_ONE;
  }

  /**
   * @inheritDoc.
   */
  public function getButtons() {
    return [
      new StepOneNextButton(),
      new StepExistingFinishButton(),
    ];
  }

  /**
   * @inheritDoc.
   */
  public function buildStepFormElements() {
    // Define whether we are adding or updating.
    $add_entity = TRUE;
    if (isset($_GET['widget_instance_id'])) {
      $add_entity = FALSE;
    }

    $form = [];

    $form['delta'] = ['#type' => 'hidden', '#value' => (int) $_GET['delta']];
    $form['widget_instance_id'] = [
      '#type' => 'hidden',
      '#value' => (isset($_GET['widget_instance_id']) ? (int) $_GET['widget_instance_id'] : 0)
    ];

    if (!$add_entity) {
      $form['title'] = [
        '#type' => 'html_tag',
        '#tag' => 'h2',
        '#value' => t('Edit Widget'),
        '#attributes' => [
          'class' => [
            'widget-title'
          ],
        ],
      ];
    }
    else {
      $form['title'] = [
        '#type' => 'html_tag',
        '#tag' => 'h2',
        '#value' => t('Add a New Widget'),
        '#attributes' => [
          'class' => [
            'widget-title'
          ],
        ],
      ];
      $form['begin_wrapper'] = ['#markup' => '<div id="tabs-widget-wrapper" class="widget-wrap">'];
      $form['tabs'] = [
        '#theme' => 'item_list',
        '#items' => [
          Link::fromTextAndUrl(t('New Widget'), Url::fromUri('internal:', ['fragment' => 'tabs-new-widget'])),
          Link::fromTextAndUrl(t('Existing Widget'), Url::fromUri('internal:', ['fragment' => 'tabs-existing-widget'])),
        ],
      ];
    }

    $form['tab_new_widget'] = ['#markup' => '<div id="tabs-new-widget" class="widget-wrap">'];

    // Send the default values as a setting to the JS.
    $form['#attached']['drupalSettings']['stacks']['form']['default_widget_type'] = isset($this->getValues()['widget_type']) ? $this->getValues()['widget_type'] : '';
    $form['#attached']['drupalSettings']['stacks']['form']['default_widget_template'] = isset($this->getValues()['widget_template']) ? $this->getValues()['widget_template'] : '';
    $form['#attached']['drupalSettings']['stacks']['form']['default_widget_theme'] = isset($this->getValues()['widget_theme']) ? $this->getValues()['widget_theme'] : '';

    $form['widget_type'] = [
      '#type' => 'select',
      '#title' => t('Widget Type'),
      '#options' => $this->selectWidgetType(),
      '#attributes' => ['id' => 'widget_type_select'],
      '#default_value' => isset($this->getValues()['widget_type']) ? $this->getValues()['widget_type'] : '',
      '#required' => FALSE,
      '#disabled' => isset($this->getValues()['widget_type']) ? TRUE : FALSE,
    ];

//    // Providing an AJAX callback when Automatic Titles is enabled. This function resolves the name for the widget.
//    // Sub 37 Image 1
//    if ($automatic_titles) {
//      $form['widget_type']['#ajax'] = [
//        'callback' => [$this, 'widgetNameChecker'],
//        'event' => 'change',
//        'progress' => [
//          'type' => 'throbber',
//          'message' => t('Filtering') . "...",
//        ],
//      ];
//
//
//      $test = 0;
//    }

    // Add the template message div.
    $form['template-message'] = [
      '#type' => 'html_tag',
      '#tag' => 'div',
      '#attributes' => [
        'id' => 'template-message',
        'class' => [
          'clearfix'
        ],
      ],
      '#value' => '',
    ];

    // Add the template radio options.
    $form['template_radios'] = $this->radiosTemplate();

    $form['div_template'] = [
      '#type' => 'html_tag',
      '#tag' => 'div',
      '#attributes' => [
        'class' => [
          'clearfix'
        ],
      ],
    ];

    // Add the theme radio options.
    $form['theme_radios'] = $this->radiosTheme();

    $form['div_theme'] = [
      '#type' => 'html_tag',
      '#tag' => 'div',
      '#attributes' => [
        'class' => [
          'clearfix'
        ],
      ],
    ];

    $widget_instance_entity = null;
    $reusable_disabled = FALSE;
    if (!empty($_GET['widget_instance_id'])) {
      $widget_instance_entity = WidgetInstanceEntity::load($_GET['widget_instance_id']);
      if ($widget_instance_entity->getTimesUsed($_GET['entity-id'])) {
        $reusable_disabled = TRUE;
      }
    }

    // Reusable widget checkbox
    $form['reusable'] = [
      '#type' => 'checkbox',
      '#title' => t('Reuse this widget'),
      '#default_value' => isset($this->getValues()['reusable']) ? $this->getValues()['reusable'] : false,
      '#disabled' => $reusable_disabled,
      '#description' => $reusable_disabled ? t('You cannot edit this because it\'s currently being used on another content.') : '',
    ];

    $form['widget_name'] = [
      '#type' => 'textfield',
      '#title' => t('Widget Name'),
      '#placeholder' => t('Widget Name'),
      '#default_value' => isset($this->getValues()['widget_name']) ? $this->getValues()['widget_name'] : '',
      '#required' => FALSE,
    ];

    // Disabling and hiding Widget name field.
    $form['widget_name']['#states'] = [
      'visible' => [
        ':input[name="reusable"]' => array('checked' => TRUE),
      ],
      'required' => [
        ':input[name="reusable"]' => array('checked' => TRUE),
      ],
      'empty' => [
        ':input[name="reusable"]' => array('checked' => TRUE),
      ],
    ];

    $form['tab_new_widget_end'] = ['#markup' => '</div>'];

    // Existing Stacks.
    if ($add_entity) {
      $form['tab_existing_widget'] = ['#markup' => '<div id="tabs-existing-widget">'];
      $form['existing_stacks'] = $this->existingStacks();
      $form['tab_existing_widget_end'] = ['#markup' => '</div>'];
    }

    $form['end_wrapper'] = ['#markup' => '</div>'];

    return $form;
  }

  /**
   * @inheritDoc.
   */
  public function getFieldNames() {
    return [
      'delta',
      'widget_instance_id',
      'widget_type',
      'widget_name',
      'widget_template',
      'widget_theme',
      'status',
      'bundle',
      'template',
      'has_templates',
      'has_themes',
      'reusable'
    ];
  }

  /**
   * @inheritDoc.
   */
  public function getFieldsValidators() {
    return [
      'widget_type' => [
        new ValidatorRequired(t("Widget Type field is required.")),
      ],
      'widget_name' => [
        new ValidatorCustom(t("Widget Name field is required."), function ($form_state, $field_value) {
          // We only want to throw an error when the following is true:
          // - widget_name is empty.
          // - reusable is checked.

          // This only requires the widget label field when the reusable option
          // is checked.
          if (empty($field_value) && $form_state->getValue('reusable')) {
            return FALSE;
          }

          return TRUE;

        }),
      ],
      'widget_template' => [
        new ValidatorCustom(t("Widget Template field is required."), function ($form_state, $field_value) {
          // We only want to throw an error when the following is true:
          // - widget_template is empty.
          // - widget_type is not empty.

          // This prevents multiple errors from showing up when the template
          // field is not visible.
          if (empty($field_value) && !empty($form_state->getValue('widget_type'))) {
            return FALSE;
          }

          return TRUE;

        }),
      ],
    ];
  }

  /**
   * Load widget instance entity and stacks entity and then prepopulate the form.
   *
   * @param $entities - The widget instance and widget entities.
   */
  public function editWidgetInstance($entities) {
    $widget_instance = $entities['widget_instance_entity'];
    $widget_entity = $entities['widget_entity'];
    $bundle = $widget_entity->bundle();

    // Define the options we are loading. This needs to be all the available
    // options in the first step of the form.
    $default_values = [
      'delta' => $_GET['delta'],
      'widget_instance_id' => $widget_instance->id(),
      'widget_type' => $widget_entity->getWidgetType(),
      'widget_name' => $widget_instance->getTitle(),
      'widget_template' => $bundle . '--' . $widget_instance->getTemplate(),
      'widget_theme' => $widget_instance->getTheme(),
      'status' => $widget_instance->getStatus(),

      // These values are used to access this info throughout the process.
      'bundle' => $bundle,
      'template' => $widget_instance->getTemplate(),
      'has_themes' => $this->selectTheme($bundle),

      // Reusable/Shared widget
      'reusable' => $widget_instance->isShareable(),
    ];

    // Now save the values for this step. This should pre-populate the form.
    $this->setValues($default_values);
    // has_templates depends on some of the values set above when editing a
    // widget instance
    $default_values += [
      'has_templates' => $this->radiosTemplate($bundle),
    ];
    $this->setValues($default_values);
  }

  /**
   * Return options for the Widget Type dropdown.
   *
   * @return array
   */
  static public function selectWidgetType() {
    // Make sure there is at least one bundle option for this content type.
    $bundles = EntityFormDisplay::load($_GET['entity-type'] . '.' . $_GET['content-type'] . '.default')
      ->getComponent($_GET['field'])['settings'];
    $bundles = (isset($bundles['bundles']) ? $bundles['bundles'] : []);

    // Getting widget groups to wrap template variants.
    $config = \Drupal::service('config.factory')->getEditable('stacks.settings');
    $widget_type_groups = $config->get("widget_type_groups");

    if (empty($bundles)) {
      \Drupal::messenger()->addError(t('No widgets available for this content type.'));
    }

    $widget_type_options = ['' => t('-- Choose a Widget Type --')];

    $widget_type_manager = \Drupal::service('plugin.manager.stacks_widget_type');

    foreach ($bundles as $bundle) {
      if (empty($bundle)) {
        continue;
      }
      $widget_entity_type = WidgetEntityType::load($bundle);
      if (isset($widget_entity_type) && $widget_type_manager->hasDefinition($widget_entity_type->getPlugin())) {
        $id = '';
        $label = '';

        // Checking if the bundle is standalone or grouped with other widget types.
        if (is_array($widget_type_groups) && count($widget_type_groups) > 0) {
          foreach ($widget_type_groups as $group_id => $group_label) {
            if(substr($bundle, 0, strlen($group_id)) == $group_id) {
              $id = $group_id;
              $label = $group_label;
            }
          }
        }

        if (empty($id) && empty($label)) {
          $id = $widget_entity_type->id();
          $label = $widget_entity_type->label();
        }

        $widget_type_options[$id] = $label;

      }
    }

    asort($widget_type_options);
    return $widget_type_options;
  }

  /**
   * Return options for the Template radio buttons.
   *
   * @param $templates_by_widget_type string/bool Widget type or FALSE
   *
   * @return bool/array If the widget type is present return whether there are
   * variations for that widget type. If false, return an array of template
   * variations
   */
  public function radiosTemplate($templates_by_widget_type = FALSE) {
    $templates = WidgetTemplates::getTemplatesSelect();
    $bundles_get = \Drupal::service('entity_type.bundle.info')->getBundleInfo('widget_entity');

    $config = \Drupal::service('config.factory')->getEditable('stacks.settings');
    $widget_type_groups = $config->get("widget_type_groups");

    // Add hidden divs for template options for all widgets.
    $template_options = [];
    $base_dir_stacks = WidgetTemplates::templateDir();
    foreach ($templates as $bundle => $bundle_templates) {

      $widget_group = $bundle;

      // Check if this belongs to a group
      $is_group = FALSE;
      if(is_array($widget_type_groups) && count($widget_type_groups)) {
        foreach($widget_type_groups as $group => $value) {
          if(substr($bundle, 0, strlen($group)) == $group) {
            $widget_group = $group;
            $is_group = TRUE;
            break;
          }
        }
      }

      // If we are targeting a certain widget type, continue if this is not that
      // type.
      if ($templates_by_widget_type && $widget_group != $templates_by_widget_type) {
        continue;
      }

      foreach ($bundle_templates as $bundle_template => $bundle_template_display) {

        if ($is_group) {
          // This is a widget group. We need to make sure to display the bundle
          // label for these cases.
          if ($bundle_template_display != 'Default') {
            $bundle_template_display = $bundles_get[$bundle]['label'] . ' - ' . $bundle_template_display;
          }
          else {
            $bundle_template_display = $bundles_get[$bundle]['label'];
          }

          // If we are editing an instance of a grouped widget type, discard
          // other widget templates except those variations of the same widget
          // type
          $widget_instance_bundle = $this->getValues()['bundle'];
          if (!empty($widget_instance_bundle)) {
            if (!preg_match('/^' . preg_quote($widget_instance_bundle) . '$/', $bundle)) {
              continue;
            }
          }
        }

        // Define the preview image.
        $renamed_dir = str_replace(['_', ' '], '-', $bundle);
        $renamed_template = str_replace(['_', ' '], '-', $bundle_template);
        $preview_image_file = DRUPAL_ROOT . '/' . $base_dir_stacks . '/' . $renamed_dir . '/images/' . $renamed_template;

        $preview_images = array_merge(glob($preview_image_file . '.{jpg,jpeg,png,gif}', GLOB_BRACE), glob($preview_image_file . '.{JPG,JPEG,PNG,GIF}', GLOB_BRACE));

        if (!empty($preview_images)) {
          $preview_image_file = substr($preview_images[0], strlen(DRUPAL_ROOT));
          $preview_image_array = [
            '#type' => 'container',
            'img' => [
              '#type' => 'html_tag',
              '#tag' => 'img',
              '#attributes' => [
                'width' => 241,
                'src' => $preview_image_file,
              ],
            ],
          ];
        }
        else {
          $preview_image_array = [
            '#type' => 'container',
            'img' => [
              '#type' => 'html_tag',
              '#tag' => 'img',
              '#attributes' => [
                'width' => 241,
                'height' => 234,
                'title' => '',
                'alt' => '',
                'src' => '/' . \Drupal::service('extension.list.module')->getPath('stacks') . '/images/no-preview-img.png',
              ],
            ],
          ];
        }

        $preview_image = \Drupal::service('renderer')->render($preview_image_array);

        $machine_name = $bundle . '--' . $bundle_template;

        // If the widget type belongs to a group, then the radio element for this option will have the group attribute.
        if ($is_group) {
          $bundle = $widget_group;
        }

        // Create the radio button.
        $template_options[$machine_name]['widget_template'] = [
          '#type' => 'radio',
          '#title' => $bundle_template_display,
          '#return_value' => $machine_name,
          '#default_value' => '',
          '#field_prefix' => $preview_image,
          '#prefix' => '<div id="template-' . $machine_name . '" class="widget_template_radio" group="' . $bundle . '">',
          '#suffix' => '</div>',
        ];

      }
    }

    // If $templates_by_widget_type is true, return TRUE/FALSE if there are
    // templates for this widget type.
    if ($templates_by_widget_type) {
      return count($template_options) > 1;
    }

    return $template_options;
  }

  /**
   * @return array
   */
  public function radiosTheme() {
    $config = \Drupal::service('config.factory')
      ->getEditable('stacks.settings');
    $themes = $config->get("template_themes_config");

    if (!is_array($themes)) {
      return [];
    }

    // Add hidden divs for template options for all widgets.
    $template_options = [];
    $base_dir_stacks = WidgetTemplates::templateDir();
    foreach ($themes as $bundle => $bundle_themes) {

      foreach ($bundle_themes as $bundle_template => $bundle_theme_options) {

        foreach ($bundle_theme_options as $option_key => $option_value) {

          // Define the preview image.
          $renamed_dir = str_replace(['_', ' '], '-', $bundle);
          $renamed_template = str_replace(['_', ' '], '-', $bundle_template);
          $renamed_option = str_replace(['_', ' '], '-', $option_key);
          $preview_image_file = DRUPAL_ROOT . '/' . $base_dir_stacks . '/' . $renamed_dir . '/images/' . $renamed_template . '--' . $renamed_option;

          $theme_images = array_merge(glob($preview_image_file . '.{jpg,jpeg,png,gif}', GLOB_BRACE), glob($preview_image_file . '.{JPG,JPEG,PNG,GIF}', GLOB_BRACE));

          if (!empty($theme_images)) {
            $preview_image_file = substr($theme_images[0], strlen(DRUPAL_ROOT));
            $preview_image_array = [
              '#type' => 'container',
              'img' => [
                '#type' => 'html_tag',
                '#tag' => 'img',
                '#attributes' => [
                  'width' => 100,
                  'title' => $option_value,
                  'alt' => $option_value,
                  'src' => $preview_image_file,
                ],
              ],
            ];
          }
          else {
            $preview_image_array = [
              '#type' => 'container',
              'img' => [
                '#type' => 'html_tag',
                '#tag' => 'img',
                '#attributes' => [
                  'width' => 100,
                  'title' => $option_value,
                  'alt' => $option_value,
                  'src' => '/' . \Drupal::service('extension.list.module')->getPath('stacks') . '/images/no-preview-img.png',
                ],
              ],
            ];
          }

          $preview_image = \Drupal::service('renderer')->render($preview_image_array);

          $machine_name = $bundle . '--' . $bundle_template . '--' . $option_key;

          // Create the radio button.
          $template_options[$machine_name]['widget_theme'] = [
            '#type' => 'radio',
            '#title' => $option_value,
            '#return_value' => $option_key,
            '#default_value' => '',
            '#field_prefix' => $preview_image,
            '#prefix' => '<div id="theme-' . $machine_name . '" class="widget_theme_radio" bundle-template="' . $bundle . '--' . $bundle_template . '">',
            '#suffix' => '</div>',
          ];

        }
      }
    }

    return $template_options;

  }

  /**
   * Return options for the Theme select.
   *
   * @param $templates_by_bundle string/bool Widget type or FALSE
   *
   * @return bool/array If the widget type is present return whether there are
   * theme options for that widget type. If false, return an array theme options
   */
  public function selectTheme($templates_by_bundle = FALSE) {
    $config = \Drupal::service('config.factory')
      ->getEditable('stacks.settings');
    $themes = $config->get("template_themes_config");

    $theme_options = [];
    if (is_array($themes)) {
      foreach ($themes as $bundle => $bundle_templates) {

        // If $templates_by_bundle is set, we only want theme options for this
        // bundle.
        if ($templates_by_bundle && $bundle != $templates_by_bundle) {
          continue;
        }

        foreach ($bundle_templates as $template_themes) {
          foreach ($template_themes as $theme => $theme_display) {
            $theme_options[$theme] = $theme_display;
          }
        }
      }
    }

    // If $templates_by_bundle is true, return TRUE/FALSE if there are templates
    // for this widget type.
    if ($templates_by_bundle) {
      return count($theme_options) > 0;
    }

    return $theme_options;
  }

  /**
   *
   * Return a render array.
   */
  public function existingStacks() {
    // Create search filters.
    $form['filter_title_search'] = [
      '#placeholder' => t('Search widgets'),
      '#type' => 'textfield',
      '#default_value' => isset($this->getValues()['filter_title_search']) ? $this->getValues()['filter_title_search'] : '',
      '#attributes' => [
        'placeholder' => t('Search widgets'),
        'class' => ['title-search'],
      ],
      '#ajax' => [
        'callback' => [$this, 'filterExistingWidgets'],
        'event' => 'debounce_filter',
        'progress' => [
          'type' => 'throbber',
          'message' => t('Filtering') . "...",
        ],
      ],
    ];

    // Setting Type array.
    $available_types = [];
    $available_types[""] = t("All Types");

    $widget_types = \Drupal::service('entity_type.bundle.info')->getBundleInfo('widget_entity');

    foreach ($widget_types as $key => $value) {
      $index = trim($key);
      if (!empty($index)) {
        $available_types[$key] = $value['label'];
      }
    }

    $form['filter_widget_type'] = [
      '#type' => 'select',
      '#options' => $available_types,
      '#default_value' => isset($this->getValues()['widget_type']) ? $this->getValues()['widget_type'] : '',
      '#ajax' => [
        'callback' => [$this, 'filterExistingWidgets'],
        'event' => 'change',
        'progress' => [
          'type' => 'throbber',
          'message' => t('Filtering') . "...",
        ],
      ],
    ];

    $form['filter_additional'] = [
      '#type' => 'select',
      '#options' => [
        'widget_times_used' => t('Most Used'),
        'title' => t('Title')
      ],
      '#default_value' => '',
      '#ajax' => [
        'callback' => [$this, 'filterExistingWidgets'],
        'event' => 'change',
        'progress' => [
          'type' => 'throbber',
          'message' => t('Filtering') . "...",
        ],
      ],
    ];

    $form['filter_results_per_page'] = [
      '#type' => 'select',
      '#options' => [
//        10 => 'Results per page',
        10 => '10',
        25 => '25',
        50 => '50',
      ],
      '#default_value' => '',
      '#ajax' => [
        'callback' => [$this, 'filterExistingWidgets'],
        'event' => 'change',
        'progress' => [
          'type' => 'throbber',
          'message' => t('Filtering') . "...",
        ],
      ],
    ];

    $form['table_pager'] = [
      '#weight' => 1,
      '#type' => 'textfield',
      '#attributes' => ['class' => ['table_pager_element']],
      '#default_value' => isset($this->getValues()['table_pager']) ? $this->getValues()['table_pager'] : '0',
      '#ajax' => [
        'callback' => [$this, 'filterExistingWidgets'],
        'event' => 'change',
      ],
    ];

    // Because the existing stacks table is being replaces via ajax alone and
    // not through in a proper FAPI way, this hacks prevents an "An illegal
    // choice has been detected. Please contact the site administrator." error
    // on the radios. We are querying $_POST details directly so this should
    // probably be address later on.
    $existing_stacks_table_data = [
      'filter_title_search',
      'filter_widget_type',
      'filter_additional',
      'table_pager',
      'filter_results_per_page',
    ];
    $existing_stacks_table_args = [
      '',
      '',
      'widget_times_used',
      0,
      10
    ];
    foreach ($existing_stacks_table_data as $k => $el) {
      $existing_stacks_table_args[$k] = \Drupal::request()->request->get(
        $el,
        $existing_stacks_table_args[$k]
      );
    }
    $form['existing_stacks_table'] = call_user_func_array(
      '_stacks_get_existing_stacks_table',
      $existing_stacks_table_args
    );

    return $form;
  }

  /**
   * @inheritDoc.
   */
  public function filterExistingWidgets($form, &$form_state) {
    // Getting current values.
    $search = trim($form_state->getValue('filter_title_search'));
    $type = $form_state->getValue('filter_widget_type');
    $sort = $form_state->getValue('filter_additional');
    $pager = $form_state->getValue('table_pager');
    $limit = $form_state->getValue('filter_results_per_page');
    $page_number = 0;

    // Getting pager
    if (!empty($pager) && is_numeric($pager) && $pager >= 0) {
      $page_number = $pager;
    }

    $newtable = _stacks_get_existing_stacks_table($search, $type, $sort, $page_number, $limit, TRUE);

    $form = [];

    $form['existing_stacks_table'] = $newtable;
    $form['existing_stacks_table']['#parents'] = [];

    Tableselect::processTableselect($form['existing_stacks_table'], $form_state, $form);
    // This is a bit of hack to prevent a PHP warning.
    // When I looked into this I tried to understand the root cause, but this is
    // not quite doing the form ajax replacement the Drupal way as far as I could
    // gather, and being that this work, I figured this workaround was OK.
    foreach (Element::children($form['existing_stacks_table']) as $key) {
      if ($form['existing_stacks_table'][$key]['#type'] == 'radio' && !isset($form['existing_stacks_table'][$key]['#value'])) {
        $form['existing_stacks_table'][$key]["#value"] = '';
      }
    }

    // Adjust some attributes. #name is the important one
    foreach ($form['existing_stacks_table'] as $key => &$item) {
      if (is_numeric($key) && !empty($item['#type']) && $item['#type'] == 'radio') {
        $item['#attributes']['data-drupal-selector'] = 'edit-existing-widgets-table-' . $key . '-x';
        $item['#name'] = 'existing_stacks_table';
      }
    }

    $response = new AjaxResponse();
    $response->addCommand(new ReplaceCommand(".existing_stacks_dashboard", $form));

    return $response;
  }
}

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

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