Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
novicell/custom_forms / modules / states / src / Element / StateElement.php
Size: Mime:
<?php

namespace Drupal\custom_forms_states\Element;

use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\OptGroup;
use Drupal\Core\Render\Element\FormElement;
use Drupal\Core\Serialization\Yaml;

/**
 * Class StateElement
 *
 * @package Drupal\custom_forms_states\Element
 *
 * @FormElement("state_element")
 */
class StateElement extends FormElement {

  /**
   * {@inheritdoc}
   */
  public function getInfo() {
    $class = get_class($this);
    return [
      '#input' => TRUE,
      '#selector_options' => [],
      '#empty_states' => 3,
      '#process' => [
        [$class, 'processStateElement'],
      ],
      '#theme_wrappers' => ['form_element'],
      '#multiple' => TRUE,
    ];
  }

  /**
   * {@inheritdoc}
   */
  public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
    if ($input === FALSE) {
      if (isset($element['#default_value'])) {
        if (is_string($element['#default_value'])) {
          $default_value = Yaml::decode($element['#default_value']);
        }
        else {
          $default_value = $element['#default_value'] ?: [];
        }
        return $default_value;
      }
      else {
        return [];
      }
    }
    elseif (is_array($input) && isset($input['states'])) {
      return is_string($input['states']) ? Yaml::decode($input['states']) : $input['states'];
    }
    else {
      return [];
    }
  }

  /**
   * @param $element
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   * @param $complete_form
   *
   * @return array
   */
  public static function processStateElement(&$element, FormStateInterface $form_state, &$complete_form) {
    $element += [
      '#state_options' => static::getStateOptions(),
      '#trigger_options' => static::getTriggerOptions(),
    ];
    $element['#state_options_flattened'] = OptGroup::flattenOptions($element['#state_options']);
    $element['#selector_options_flattened'] = OptGroup::flattenOptions($element['#selector_options']);
    $element['#tree'] = TRUE;

    $table_id = implode('_', $element['#parents']) . '_wrapper';

    $ajax_settings = [
      'callback' => [static::class, 'ajaxCallback'],
      'wrapper' => $table_id,
      'progress' => ['type' => 'none'],
    ];

    $element['add_state'] = [
      '#type' => 'submit',
      '#value' => t('Add state'),
      '#limit_validation_errors' => [],
      '#submit' => [[static::class, 'addSubmit']],
      '#ajax' => $ajax_settings,
      '#name' => $table_id . '_add_state',
      '#action_type' => 'add-state',
      '#attributes' => [
        'class' => [
          'add-state-button',
        ]
      ],
    ];

    $storage_key = 'element_states__' . $element['#name'] . '__number_of_rows';
    if ($form_state->get($storage_key) === NULL) {
      if (empty($element['#default_value']) || !is_array($element['#default_value'])) {
        $number_of_rows = 1;
      }
      else {
        $number_of_rows = count($element['#default_value']);
      }
      $form_state->set($storage_key, $number_of_rows);
    }
    $number_of_rows = $form_state->get($storage_key);

    if ($form_state->isRebuilding()) {
      $states = $element['#value'];
    }
    else {
      $states = $element['#default_value'] ?? [];
    }

    // Build state rows.
    $row_index = 0;
    $rows = [];
    foreach ($states as $state_settings) {
      $rows[$row_index] = self::buildStateRow($element, $state_settings, $table_id, $row_index, $form_state, $ajax_settings);
      $row_index++;
    }

    // Generator empty state with conditions rows.
    if ($row_index < $number_of_rows) {
      while ($row_index < $number_of_rows) {
        $rows[$row_index] = self::buildStateRow($element, [], $table_id, $row_index, $form_state, $ajax_settings);
        $row_index++;
      }
    }

    // Add wrapper to the element.
    $element += ['#prefix' => '', '#suffix' => ''];
    $element['#prefix'] = '<div id="' . $table_id . '" class="element-states">' . $element['#prefix'];
    $element['#suffix'] .= '</div>';

    // Build table.
    $element['states'] = [
        '#type' => 'container',
        '#attributes' => ['class' => ['states-container']],
      ] + $rows;

    return $element;
  }

  /**
   * Build state row.
   *
   * @param array $element
   *   The element.
   * @param array $state
   *   The state.
   * @param string $table_id
   *   The element's table id.
   * @param int $row_index
   *   The row index.
   * @param array $ajax_settings
   *   An array containing Ajax callback settings.
   *
   * @return array
   *   A render array containing a state table row.
   */
  protected static function buildStateRow(array $element, array $state, $table_id, $row_index, FormStateInterface $form_state, array $ajax_settings) {
    $state += ['state' => '', 'operator' => 'and', 'value' => '', 'condition' => []];
    $element_name = $element['#name'];
    $state_selector = ":input[name=\"{$element_name}[states][{$row_index}][state]\"]";

    $row = [
      '#type' => 'html_tag',
      '#tag' => 'div',
      '#attributes' => [
        'class' => ['element-states--state'],
      ],
    ];
    $row['state'] = [
      '#type' => 'select',
      '#title' => t('State'),
      '#title_display' => 'invisible',
      '#options' => $element['#state_options'],
      '#default_value' => $state['state'] ?? '',
      '#empty_option' => t('- Select -'),
      '#parents' => [$element_name, 'states', $row_index , 'state'],
      '#wrapper_attributes' => ['class' => ['element-states--state--state']],
      '#field_prefix' => t('Set'),
      '#error_no_message' => TRUE,
      '#attributes' => [
        'title' => t('The state to apply when the conditions below are met.')
      ]
    ];
    $row['value'] = [
      '#type' => 'textfield',
      '#title' => t('Value'),
      '#title_display' => 'invisible',
      '#size' => 20,
      '#placeholder' => t('Enter value…'),
      '#default_value' => $state['value'] ?? '',
      '#parents' => [$element_name, 'states', $row_index , 'value'],
      '#wrapper_attributes' => ['class' => ['element-states--state--value']],
      '#field_prefix' => '=',
      '#states' => [
        'visible' => [
          $state_selector => ['value' => 'value'],
        ],
      ],
      '#error_no_message' => TRUE,
    ];

    if ($element['#multiple']) {
      $row['operations'] = self::buildOperations($table_id, $row_index, $ajax_settings);
    } else {
      $row['operations'] = [];
    }

    $row['break'] = [
      '#type' => 'html_tag',
      '#tag' => 'div',
      '#attributes' => [
        'class' => ['element-states--state--break'],
      ]
    ];
    $row['operator'] = [
      '#type' => 'select',
      '#title' => t('Operator'),
      '#title_display' => 'invisible',
      '#options' => [
        'and' => t('All'),
        'or' => t('Any'),
        'xor' => t('One'),
      ],
      '#attributes' => [
        'title' => t('The operator used when validating multiple conditions.'),
      ],
      '#default_value' => $state['operator'] ?? 'and',
      '#parents' => [$element_name, 'states', $row_index , 'operator'],
      '#field_prefix' => t('If'),
      '#field_suffix' => t('of the following is met:'),
      '#wrapper_attributes' => ['class' => ['element-states--state--operator']],
      '#error_no_message' => TRUE,
    ];
    $row['add_condition'] = [
      '#type' => 'submit',
      '#value' => t('Add condition'),
      '#limit_validation_errors' => [],
      '#submit' => [[static::class, 'addSubmit']],
      '#ajax' => $ajax_settings,
      '#name' => $table_id . '_state_'. $row_index . '_add_condition',
      '#action_type' => 'add-condition',
      '#row_index' => $row_index,
      '#attributes' => [
        'class' => [
          'add-condition-button',
        ]
      ],
    ];

    // Conditions
    $storage_key = 'element_states__' . $element['#name'] . '__state_' . $row_index . '__conditions__number_of_rows';
    if ($form_state->get($storage_key) === NULL) {
      if (empty($state['condition']) || !is_array($state['condition'])) {
        $number_of_rows = 1;
      }
      else {
        $number_of_rows = count($state['condition']);
      }
      $form_state->set($storage_key, $number_of_rows);
    }
    $number_of_rows = $form_state->get($storage_key);
    $condition_count = 0;
    foreach ($state['condition'] as $condition) {
      $row[$condition_count] = self::buildConditionRow($element, $condition, $table_id, $row_index, $condition_count, $ajax_settings);
      $condition_count++;
    }

    // Generator empty state with conditions rows.
    if ($condition_count < $number_of_rows) {
      while ($condition_count < $number_of_rows) {
        $row[$condition_count] = self::buildConditionRow($element, [], $table_id, $row_index, $condition_count, $ajax_settings);
        $condition_count++;
      }
    }

    return $row;
  }

  /**
   * Build condition row.
   *
   * @param array $element
   *   The element.
   * @param array $condition
   *   The condition.
   * @param string $table_id
   *   The element's table id.
   * @param int $row_index
   *   The row index.
   * @param array $ajax_settings
   *   An array containing Ajax callback settings.
   *
   * @return array
   *   A render array containing a condition table row.
   */
  protected static function buildConditionRow(array $element, array $condition, $table_id, $state_index, $row_index, array $ajax_settings) {
    $element_name = $element['#name'];

    $row['condition'] = [
      '#type' => 'html_tag',
      '#tag' => 'div',
      '#attributes' => [
        'class' => ['element-state-conditions--condition'],
      ],
    ];
    $trigger_selector = ":input[name=\"{$element_name}[states][{$state_index}][condition][{$row_index}][trigger]\"]";
    $row['condition']['selector'] = [
      '#type' => 'select',
      '#title' => t('Selector'),
      '#title_display' => 'invisible',
      '#options' => $element['#selector_options'],
      '#wrapper_attributes' => ['class' => ['element-state-conditions--condition--selector']],
      '#default_value' => $condition['selector'] ?? '',
      '#empty_option' => t('- Select -'),
      '#parents' => [$element_name, 'states', $state_index, 'condition', $row_index , 'selector'],
      '#error_no_message' => TRUE,
      '#attributes' => [
        'title' => t('The field or group to check the condition against.')
      ],
    ];
    if (!empty($condition['selector']) && !isset($element['#selector_options_flattened'][$condition['selector']])) {
      $row['condition']['selector']['#options'][$condition['selector']] = $condition['selector'];
    }
    $row['condition']['trigger'] = [
      '#type' => 'select',
      '#title' => t('Trigger'),
      '#title_display' => 'invisible',
      '#options' => $element['#trigger_options'],
      '#default_value' => $condition['trigger'] ?? '',
      '#empty_option' => t('- Select -'),
      '#parents' => [$element_name, 'states', $state_index, 'condition', $row_index , 'trigger'],
      '#wrapper_attributes' => ['class' => ['element-state-conditions--condition--trigger']],
      '#error_no_message' => TRUE,
      '#attributes' => [
        'title' => t('The condition the selected field or group must meet.')
      ],
    ];
    $row['condition']['value'] = [
      '#type' => 'textfield',
      '#title' => t('Value'),
      '#title_display' => 'invisible',
      '#size' => 25,
      '#default_value' => $condition['value'] ?? '',
      '#placeholder' => t('Enter value…'),
      '#states' => [
        'visible' => [
          [$trigger_selector => ['value' => 'value']],
          'or',
          [$trigger_selector => ['value' => '!value']],
          'or',
          [$trigger_selector => ['value' => 'pattern']],
          'or',
          [$trigger_selector => ['value' => '!pattern']],
          'or',
          [$trigger_selector => ['value' => 'greater']],
          'or',
          [$trigger_selector => ['value' => 'less']],
        ],
      ],
      '#wrapper_attributes' => ['class' => ['element-state-conditions--condition--value']],
      '#parents' => [$element_name, 'states', $state_index, 'condition', $row_index , 'value'],
      '#error_no_message' => TRUE,
    ];
    if ($element['#multiple']) {
      $row['condition']['operations'] = self::buildConditionOperations($table_id, $state_index, $row_index, $ajax_settings);
    }
    return $row;
  }

  /**
   * Build a state's operations.
   *
   * @param string $table_id
   *   The option element's table id.
   * @param int $row_index
   *   The option's row index.
   * @param array $ajax_settings
   *   An array containing Ajax callback settings.
   *
   * @return array
   *   A render array containing state operations.
   */
  protected static function buildOperations($table_id, $row_index, array $ajax_settings) {
    $operations = [
      '#type' => 'html_tag',
      '#tag' => 'div',
      '#attributes' => ['class' => ['element-states--state--operations']],
    ];
    $operations['remove'] = [
      '#type' => 'image_button',
      '#title' => t('Remove this state.'),
      '#src' => drupal_get_path('module', 'custom_forms_states') . '/images/ffffff/minus.svg',
      '#limit_validation_errors' => [],
      '#submit' => [[static::class, 'removeStateSubmit']],
      '#attributes' => ['class' => ['element-states--state--operation', 'element-states--state--operation-remove']],
      '#ajax' => $ajax_settings,
      '#row_index' => $row_index,
      '#action_type' => 'remove-state',
      '#name' => $table_id . '_remove_' . $row_index,
    ];
    return $operations;
  }

  /**
   * Build a state's operations.
   *
   * @param string $table_id
   *   The option element's table id.
   * @param int $row_index
   *   The option's row index.
   * @param array $ajax_settings
   *   An array containing Ajax callback settings.
   *
   * @return array
   *   A render array containing state operations.
   */
  protected static function buildConditionOperations($table_id, $row_index, $condition_index, array $ajax_settings) {
    $operations = [
      '#type' => 'html_tag',
      '#tag' => 'div',
      '#attributes' => ['class' => ['element-state-conditions--condition--operations']],
    ];
    $operations['remove'] = [
      '#type' => 'image_button',
      '#title' => t('Remove this condition.'),
      '#src' => drupal_get_path('module', 'custom_forms_states') . '/images/ffffff/minus.svg',
      '#limit_validation_errors' => [],
      '#submit' => [[static::class, 'removeConditionSubmit']],
      '#attributes' => ['class' => ['element-state-conditions--condition--operation', 'element-state-conditions--condition--operation-remove']],
      '#ajax' => $ajax_settings,
      '#row_index' => $row_index,
      '#action_type' => 'remove-condition',
      '#condition_index' => $condition_index,
      '#name' => $table_id . '_remove_' . $row_index . '_remove_condition_' . $condition_index,
    ];
    return $operations;
  }

  /**
   * Form submission handler for adding another state.
   *
   * @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.
   */
  public static function addSubmit(array &$form, FormStateInterface $form_state) {
    // Get the webform states element by going up one level.
    $button = $form_state->getTriggeringElement();
    if ($button['#action_type'] === 'add-condition') {
      $parent_length = -3;
    } else if ($button['#action_type'] === 'add-state') {
      $parent_length = -1;
    }
    $element =& NestedArray::getValue($form, array_slice($button['#array_parents'], 0, $parent_length));

    // The $row_index is not sequential so we need to rebuild the value instead
    // of just using an array_slice().
    $row_index = $button['#row_index'];
    $condition_index = $button['#condition_index'];
    $values = [];
    if ($button['#action_type'] === 'add-condition') {
      foreach ($element['#value'] as $index => $value) {
        if ($index == $row_index) {
          $value['condition'][] = ['selector' => '', 'trigger' => '', 'value' => ''];
        }
        $values[] = $value;
      }
    } else if ($button['#action_type'] === 'add-state') {
      foreach ($element['#value'] as $index => $value) {
        $values[] = $value;
      }
      $values[] = ['state' => '', 'operator' => 'and', 'value' => '', 'condition' => [['selector' => '', 'trigger' => '', 'value' => '']]];
    }

    // Reset values.
    $values = array_values($values);

    // Set values.
    $form_state->setValueForElement($element['states'], $values);
    NestedArray::setValue($form_state->getUserInput(), $element['states']['#parents'], $values);

    if ($button['#action_type'] === 'add-condition') {
      foreach ($values as $index => $value) {
        $form_state->set('element_states__' . $element['#name'] . '__state_' . $index . '__conditions__number_of_rows', count($value['condition']));
      }
    } else {
      // Update the number of rows.
      $form_state->set('element_states__' . $element['#name'] . '__number_of_rows', count($values));
    }

    // Rebuild the form.
    $form_state->setRebuild();
  }

  /**
   * Form submission handler for removing a state.
   *
   * @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.
   */
  public static function removeStateSubmit(array &$form, FormStateInterface $form_state) {
    $button = $form_state->getTriggeringElement();
    $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -4));

    $row_index = $button['#row_index'];
    $values = $element['#value'];

    if (isset($values[$row_index])) {
      // Remove state.
      do {
        unset($values[$row_index]);
        $row_index++;
      } while (isset($values[$row_index]) && !isset($values[$row_index]));
    }

    // Reset values.
    $values = array_values($values);

    // Set values.
    $form_state->setValueForElement($element['states'], $values);
    NestedArray::setValue($form_state->getUserInput(), $element['states']['#parents'], $values);

    // Update the number of rows.
    $form_state->set('element_states__' . $element['#name'] . '__number_of_rows', count($values));

    // Rebuild the form.
    $form_state->setRebuild();
  }

  /**
   * Form submission handler for removing a condition.
   *
   * @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.
   */
  public static function removeConditionSubmit(array &$form, FormStateInterface $form_state) {
    $button = $form_state->getTriggeringElement();
    $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -6));

    $row_index = $button['#row_index'];
    $condition_index = $button['#condition_index'];
    $values = $element['#value'];

    if (isset($values[$row_index]['condition'][$condition_index])) {
      // Remove state.
      do {
        unset($values[$row_index]['condition'][$condition_index]);
        $condition_index++;
      } while (isset($values[$row_index]['condition'][$condition_index]) && !isset($values[$row_index]['condition'][$condition_index]));
    }

    // Reset values.
    $values[$row_index]['condition'] = array_values($values[$row_index]['condition']);

    // Set values.
    $form_state->setValueForElement($element['states'], $values);
    NestedArray::setValue($form_state->getUserInput(), $element['states']['#parents'], $values);

    // Update the number of rows.
    $form_state->set('element_states__' . $element['#name'] . '__state_' . $row_index . '__conditions__number_of_rows', count($values[$row_index]['condition']));

    // Rebuild the form.
    $form_state->setRebuild();
  }

  /**
   * Ajax callback the returns the states table.
   */
  public static function ajaxCallback(array &$form, FormStateInterface $form_state) {
    $button = $form_state->getTriggeringElement();
    $parent_length = 0;
    if ($button['#action_type'] === 'add-condition') {
      $parent_length = -3;
    } else if ($button['#action_type'] === 'remove-condition') {
      $parent_length = -6;
    } else if ($button['#action_type'] === 'add-state') {
      $parent_length = -1;
    } else if ($button['#action_type'] === 'remove-state') {
      $parent_length = -4;
    }
    $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, $parent_length));
    return $element;
  }

  /**
   * Get an associative array of translated state options.
   *
   * @return array
   *   An associative array of translated state options.
   */
  public static function getStateOptions() {
    $visibility_optgroup = (string) t('Visibility');
    $state_optgroup = (string) t('State');
    $validation_optgroup = (string) t('Validation');
    $value_optgroup = (string) t('Value');
    return [
      $visibility_optgroup => [
        'visible' => t('Visible'),
        'invisible' => t('Hidden'),
      ],
      $state_optgroup => [
        'enabled' => t('Enabled'),
        'disabled' => t('Disabled'),
        'readwrite' => t('Read/write'),
        'readonly' => t('Read-only'),
        'expanded' => t('Expanded'),
        'collapsed' => t('Collapsed'),
      ],
      $validation_optgroup => [
        'required' => t('Required'),
        'optional' => t('Optional'),
      ],
      $value_optgroup => [
        'checked' => t('Checked'),
        'unchecked' => t('Unchecked'),
        'value' => t('Value'),
      ],
    ];
  }

  /**
   * Get an associative array of translated trigger options.
   *
   * @return array
   *   An associative array of translated trigger options.
   */
  public static function getTriggerOptions() {
    return [
      'empty' => t('Empty'),
      'filled' => t('Filled'),
      'checked' => t('Checked'),
      'unchecked' => t('Unchecked'),
      'expanded' => t('Expanded'),
      'collapsed' => t('Collapsed'),
      'value' => t('Value is'),
      '!value' => t('Value is not'),
      'pattern' => t('Pattern'),
      '!pattern' => t('Not Pattern'),
      'less' => t('Less than'),
      'greater' => t('Greater than'),
    ];
  }

  /**
   * Format the item's states to a #states compatible array.
   *
   * @param array $states
   *   The array of states from the item.
   *
   * @param array $element
   *   The render array for the form element.
   *
   * @return array
   *   Returns a formatted render array for the #states of a field.
   */
  public static function formatStates(array $states, array &$element): array {
    $formatted = [];

    foreach ($states as $state) {
      $conditions = [];
      foreach ($state['condition'] as $key => $condition) {
        $value = $condition['value'];
        switch ($condition['trigger']) {
          case 'empty':
          case 'filled':
          case 'checked':
          case 'unchecked':
          case 'expanded':
          case 'collapsed':
            $value = TRUE;
        }

        // Handling of the different operators (AND, OR, XOR)
        switch ($state['operator']) {
          case 'or':
            if ($state['state'] === 'value') {
              if (in_array($condition['trigger'], ['expanded', 'collapsed'])) {
                $conditions[]['summary[aria-controls=\''.$condition['selector'].'\']|'.$state['value']][$condition['trigger']] = $value;
              } else {
                $conditions[][':input[name=\''.$condition['selector'].'\']|'.$state['value']][$condition['trigger']] = $value;
              }
            } else {
              $conditions[][':input[name=\''.$condition['selector'].'\']'][$condition['trigger']] = $value;
            }
            break;
          case 'xor':
            if ($state['state'] === 'value') {
              if (in_array($condition['trigger'], ['expanded', 'collapsed'])) {
                $conditions[]['summary[aria-controls=\''.$condition['selector'].'\']|'.$state['value']][$condition['trigger']] = $value;
              } else {
                $conditions[][':input[name=\''.$condition['selector'].'\']|'.$state['value']][$condition['trigger']] = $value;
              }
            } else {
              $conditions[][':input[name=\''.$condition['selector'].'\']'][$condition['trigger']] = $value;
            }

            end($state['condition']);
            if ($key !== key($state['condition'])) {
              $conditions[] = $state['operator'];
            }
            break;
          case 'and':
          default:
          if ($state['state'] === 'value') {
            if (in_array($condition['trigger'], ['expanded', 'collapsed'])) {
              $conditions['summary[aria-controls=\''.$condition['selector'].'\']|'.$state['value']][$condition['trigger']] = $value;
            } else {
              $conditions[':input[name=\''.$condition['selector'].'\']|'.$state['value']][$condition['trigger']] = $value;
            }
          } else {
            $conditions[':input[name=\''.$condition['selector'].'\']'][$condition['trigger']] = $value;
          }
            break;
        }
      }


      $element['#attached']['library'][] = 'custom_forms_states/states';
      if ($state['state'] === 'value') {
        $element['#attached']['library'][] = 'custom_forms_states/value-state';
        // If there is already a defined value state, we don't want to override it, but add our new state to it.
        if (!empty($element['#attributes']['data-value-state'])) {
          $conditions += json_decode($element['#attributes']['data-value-state'], TRUE);
        }
        $element['#attributes']['data-value-state'] = json_encode($conditions);
      } else {
        $formatted[$state['state']][] = $conditions;
      }
    }

    return $formatted;
  }
}