Repository URL to install this package:
|
Version:
1.3.7 ▾
|
<?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;
}
}