View source
<?php
namespace Drupal\conditional_fields;
use Drupal\Component\Render\MarkupInterface;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\Core\Render\ElementInfoManager;
class ConditionalFieldsFormHelper {
public $form;
public $form_state;
public $effects;
protected $elementInfo;
protected $type;
public function __construct(ElementInfoManager $element_info, ConditionalFieldsHandlersManager $type) {
$this->elementInfo = $element_info;
$this->type = $type;
}
public function afterBuild(array $form, FormStateInterface &$form_state) {
$this->form = $form;
$this->form_state = $form_state;
if ($this
->hasConditionalFields()) {
$this
->processDependentFields()
->addJavascriptEffects()
->addValidationCallback();
}
return $this->form;
}
public function processDependentFields() {
$this->effects = [];
foreach ($this->form['#conditional_fields'] as $dependent => $dependent_info) {
$states = [];
if (!isset($dependent_info['dependees']) || empty($dependent_info['dependees'])) {
continue;
}
$dependees = $dependent_info['dependees'];
$dependent_location = array_merge([], [
$dependent,
]);
$dependent_form_field = NestedArray::getValue($this->form, $dependent_location);
$states = $this
->processDependeeFields($dependees, $dependent_form_field, $dependent_location, $states);
if (empty($states)) {
continue;
}
NestedArray::setValue($this->form, $dependent_location, $dependent_form_field);
NestedArray::setValue($this->form, array_merge($dependent_location, [
'#states',
]), $this
->mapStates($states));
}
return $this;
}
public function processDependeeFields($dependees, &$dependent_form_field = [], $dependent_location = [], $states = []) {
foreach ($dependees as $dependency) {
$dependee = $dependency['dependee'];
if (empty($this->form['#conditional_fields'][$dependee])) {
continue;
}
$dependee_info = $this->form['#conditional_fields'][$dependee];
end($dependee_info['parents']);
$unset = key($dependee_info['parents']);
if (is_int($dependee_info['parents'][$unset]) && $dependee_info['parents'][$unset] > 0) {
unset($dependee_info['parents'][$unset]);
}
$dependee_form_field = NestedArray::getValue($this->form, $dependee_info['parents']);
$options = $dependency['options'];
if (!empty($options['reset'])) {
$dependent_form_field['#conditional_fields_reset_if_untriggered'] = TRUE;
}
if (!empty($options['values']) && is_string($options['values'])) {
$options['values'] = explode("\r\n", $options['values']);
}
$options['selector'] = $this
->getSelector($options['selector'], $dependee_form_field);
$state = $this
->getState($dependee, $dependee_form_field, $options);
if (in_array($options['condition'], [
'value',
'empty',
'!empty',
'checked',
'!checked',
])) {
$dependent_form_field = $this
->elementAddProperty($dependent_form_field, '#element_validate', [
self::class,
'dependentValidate',
], 'append');
}
$states = $this
->addStateToGroup($state, $options, $states);
$selector = $this
->buildJquerySelectorForField(NestedArray::getValue($this->form, [
$dependent_location[0],
]));
$this->effects[$selector] = $this
->getEffect($options);
}
return $states;
}
public function addJavascriptEffects() {
$this->form['#attached']['library'][] = 'conditional_fields/conditional_fields';
if ($this->effects) {
$this->form['#attached']['drupalSettings']['conditionalFields'] = [
'effects' => $this->effects,
];
}
return $this;
}
public function addValidationCallback() {
$this->form['#validate'][] = [
self::class,
'formValidate',
];
$this->form_state
->setValue('conditional_fields_untriggered_dependents', []);
return $this;
}
protected function getState($dependee, array $dependee_form_field, array $options) {
$form_display = $this->form_state
->getFormObject()
->getFormDisplay($this->form_state);
$state = [];
if ($options['condition'] != 'value') {
$state = [
$options['state'] => [
$options['selector'] => [
$options['condition'] => TRUE,
],
],
];
}
else {
$field_name = explode('[', $dependee_form_field['#name']);
$dependee_form_state = isset($dependee_form_field['#field_parents'], $field_name[0], $this->form_state) ? WidgetBase::getWidgetState($dependee_form_field['#field_parents'], $field_name[0], $this->form_state) : NULL;
$dependee_form_field['#original_name'] = $field_name[0];
$dependee_display = $form_display
->getComponent($dependee);
if (is_array($dependee_display) && array_key_exists('type', $dependee_display)) {
$widget_id = $dependee_display['type'];
}
if ($field_config = FieldStorageConfig::loadByName($form_display
->getTargetEntityTypeId(), $dependee)) {
$options['field_cardinality'] = $field_config
->getCardinality();
}
if (isset($widget_id)) {
$handler_id = 'states_handler_' . $widget_id;
$handler = $this->type
->createInstance($handler_id);
$state = $handler
->statesHandler($dependee_form_field, $dependee_form_state, $options);
}
if (empty($state)) {
$default_handler = $this->type
->createInstance('states_handler_default_state');
$state = $default_handler
->statesHandler($dependee_form_field, $dependee_form_state, $options);
}
}
return $state;
}
public function hasConditionalFields() {
if (empty($this->form['#conditional_fields'])) {
return FALSE;
}
if (!method_exists($this->form_state
->getFormObject(), 'getFormDisplay')) {
return FALSE;
}
return TRUE;
}
public function getSelector($options_selector, $dependee_form_field) {
if (!$options_selector) {
$selector = $this
->buildJquerySelectorForField($dependee_form_field);
}
else {
$current_language = \Drupal::languageManager()
->getCurrentLanguage()
->getId();
$language = isset($dependee_form_field['#language']) ? $dependee_form_field['#language'] : $current_language;
$selector = str_replace('%lang', $language, $options_selector);
}
return $selector;
}
protected function addStateToGroup(array $new_states, array $options, array $states) {
foreach ($new_states as $key => $constraints) {
if (empty($states[$key][$options['grouping']])) {
$states[$key][$options['grouping']] = $constraints;
}
else {
$states[$key][$options['grouping']] = array_merge($states[$key][$options['grouping']], $constraints);
}
}
return $states;
}
public function getEffect(array $options) {
if ($options['effect'] && $options['effect'] != 'show') {
foreach ($options['effect_options'] as &$effect_option) {
if (is_numeric($effect_option)) {
$effect_option += 0;
}
}
return [
'effect' => $options['effect'],
'options' => $options['effect_options'],
];
}
return [];
}
public static function dependentValidate($element, FormStateInterface &$form_state, $form) {
if (!isset($form['#conditional_fields'])) {
return;
}
$dependent = $form['#conditional_fields'][reset($element['#array_parents'])];
$triggered = self::evaluateDependencies($dependent, $form, $form_state);
$return = FALSE;
if ($evaluated_dependencies = self::evaluateDependencies($dependent, $form, $form_state, FALSE)) {
foreach ($evaluated_dependencies[reset($dependent['field_parents'])] as $operator) {
foreach ($operator as $state => $result) {
if ($result && $state == 'visible' && $triggered || !$result && $state == '!visible' && !$triggered) {
$return = TRUE;
}
if ($result && $state == 'required' && $triggered || !$result && $state == '!required' && !$triggered) {
$return = TRUE;
$key_exists = NULL;
$input_state = NestedArray::getValue($form_state
->getValues(), $dependent['field_parents'], $key_exists);
if ($key_exists && !is_object($input_state) && isset($input_state['add_more'])) {
unset($input_state['add_more']);
}
$input_state = is_null($input_state) ? [] : $input_state;
if (isset($dependent['field_parents'][0])) {
$field = FieldStorageConfig::loadByName($form['#entity_type'], $dependent['field_parents'][0]);
}
else {
$field = NULL;
}
if (empty($input_state)) {
if (isset($element['widget']['#title'])) {
$title = $element['widget']['#title'];
}
elseif (isset($dependent['field_parents'][0])) {
$title = $dependent['field_parents'][0];
}
elseif ($field) {
$title = $field
->getName();
}
$form_state
->setError($element, t('%name is required.', [
'%name' => $title,
]));
}
}
}
}
}
if ($return) {
return;
}
$form_state_addition['parents'] = $dependent['field_parents'];
if (!empty($element['#conditional_fields_reset_if_untriggered']) && !$triggered) {
$form_state_addition['reset'] = TRUE;
}
else {
$form_state_addition['reset'] = FALSE;
}
$errors = $form_state
->getErrors();
if ($errors) {
$error_key = reset($dependent['field_parents']);
foreach ($errors as $name => $error) {
if (strpos((string) $name, $error_key) === 0) {
$field_errors[$name] = $error;
}
}
}
if (!empty($field_errors)) {
$form_state_addition['errors'] = $field_errors;
return;
}
$fiel_state_values_count = count($form_state
->getValue('conditional_fields_untriggered_dependents'));
$form_state
->setValue([
'conditional_fields_untriggered_dependents',
$fiel_state_values_count,
], $form_state_addition);
}
protected static function evaluateDependencies(array $dependent, array $form, FormStateInterface $form_state, $grouping = TRUE) {
$dependencies = $form['#conditional_fields'][reset($dependent['field_parents'])]['dependees'];
$evaluated_dependees = [];
foreach ($dependencies as $dependency) {
$dependee = $dependency['dependee'];
if (empty($form['#conditional_fields'][$dependee]['parents'])) {
continue;
}
$dependee_parents = $form['#conditional_fields'][$dependee]['parents'];
$dependee_parents_keys = array_flip($dependee_parents);
$dependee_parent = NestedArray::getValue($form, array_slice($dependee_parents, 0, $dependee_parents_keys[$dependee]));
$values = self::formFieldGetValues($dependee_parent[$dependee], $form_state);
if (isset($values['value']) && is_numeric($values['value'])) {
$values = $values['value'];
}
if (isset($dependee_parent[$dependee]['#language'], $values[$dependee_parent[$dependee]['#language']])) {
$values = $values[$dependee_parent[$dependee]['#language']];
}
if ($grouping) {
$evaluated_dependees[reset($dependent['field_parents'])][$dependency['options']['grouping']][] = self::evaluateDependency('edit', $values, $dependency['options']);
}
else {
$evaluated_dependees[reset($dependent['field_parents'])][$dependency['options']['grouping']][$dependency['options']['state']] = self::evaluateDependency('edit', $values, $dependency['options']);
}
}
if ($grouping) {
return self::evaluateGrouping($evaluated_dependees[reset($dependent['field_parents'])]);
}
return $evaluated_dependees;
}
protected static function evaluateGrouping($groups) {
$or = $and = $xor = TRUE;
if (!empty($groups['OR'])) {
$or = in_array(TRUE, $groups['OR']);
}
if (!empty($groups['AND'])) {
$and = !in_array(FALSE, $groups['AND']);
}
if (!empty($groups['XOR'])) {
$xor = array_sum($groups['XOR']) == 1;
}
return $or && $and && $xor;
}
public static function formValidate($form, FormStateInterface &$form_state) {
if (empty($form_state
->getValue('conditional_fields_untriggered_dependents'))) {
return;
}
$entity = $form_state
->getFormObject()
->getEntity();
$untriggered_dependents_errors = [];
foreach ($form_state
->getValue('conditional_fields_untriggered_dependents') as $field) {
$parent = [
$field['parents'][0],
];
$dependent = NestedArray::getValue($form, $parent);
$field_values_location = self::formFieldGetValues($dependent, $form_state);
$dependent_field_name = reset($dependent['#array_parents']);
if (empty($field_values_location)) {
if (!empty($field['errors'])) {
$untriggered_dependents_errors = array_merge($untriggered_dependents_errors, $field['errors']);
}
continue;
}
if (!empty($field['reset'])) {
$default = $entity
->getFieldDefinition($dependent_field_name)
->getDefaultValue($entity);
$form_state
->setValue($dependent_field_name, $default);
}
if (!empty($field['errors'])) {
$untriggered_dependents_errors = array_merge($untriggered_dependents_errors, $field['errors']);
}
}
if (!empty($untriggered_dependents_errors)) {
$errors = array_diff_assoc((array) $form_state
->getErrors(), $untriggered_dependents_errors);
$form_state
->clearErrors();
$error_messages = \Drupal::messenger()
->messagesByType('error');
$removed_messages = array_values($untriggered_dependents_errors);
foreach ($errors as $name => $error) {
$form_state
->setErrorByName($name, $error);
$removed_messages[] = $error;
}
if (!empty($error_messages['error'])) {
$error_messages_array = $error_messages['error'] instanceof MarkupInterface ? $error_messages['error']
->jsonSerialize() : $error_messages['error'];
foreach (array_diff($error_messages_array, $removed_messages) as $message) {
\Drupal::messenger()
->addMessage($message, 'error');
}
}
}
}
protected static function formFieldGetValues($element, FormStateInterface $form_state) {
$parents = !empty($element['#array_parents']) ? $element['#array_parents'] : $element['#parents'];
return NestedArray::getValue($form_state
->getValues(), $parents);
}
protected static function evaluateDependency($context, $values, array $options) {
if ($options['values_set'] == ConditionalFieldsInterface::CONDITIONAL_FIELDS_DEPENDENCY_VALUES_WIDGET) {
$dependency_values = $context == 'view' ? $options['value'] : $options['value_form'];
if ($options['condition'] === '!empty') {
$values = isset($values[0]['value']) ? $values[0]['value'] : $values;
$values = $values === '_none' ? '' : $values;
return !empty($values) ? TRUE : FALSE;
}
if ($options['condition'] === 'empty') {
$values = isset($values[0]['value']) ? $values[0]['value'] : $values;
$values = $values === '_none' ? '' : $values;
return empty($values) ? TRUE : FALSE;
}
if ($options['condition'] === 'checked' || $options['condition'] === '!checked') {
$dependency_values = (int) ($options['condition'] === 'checked');
}
if (!is_array($values) || is_array($values) && empty($values)) {
$values = $values === '_none' ? '' : $values;
if (!is_array($dependency_values)) {
if (is_int($values) && is_numeric($dependency_values)) {
$dependency_values = (int) $dependency_values;
}
return $dependency_values === $values;
}
$values = [
[
'value' => $values,
],
];
}
if ($context == 'edit') {
if (!is_array($dependency_values)) {
if (is_null(current($values)) || empty($options['value'][0])) {
return FALSE;
}
$key = current(array_keys((array) current($values)));
$dependency_values = [
[
$key => $options['value'][0][$key],
],
];
$temp[][$key] = $values[0][$key];
$values = $temp;
}
return array_values($dependency_values) == array_values($values);
}
foreach ($values as $delta => &$value) {
if (isset($dependency_values[$delta])) {
$value = array_intersect_key($value, $dependency_values[$delta]);
foreach ($value as $key => &$element_value) {
if (isset($dependency_values[$delta][$key]) && is_int($dependency_values[$delta][$key]) && is_numeric($element_value)) {
$element_value = (int) $element_value;
}
}
}
}
foreach ($dependency_values as $delta => $dependency_value) {
if (!isset($values[$delta])) {
return FALSE;
}
foreach ($dependency_value as $key => $dependency_element_value) {
if (isset($values[$delta][$key]) && $values[$delta][$key] !== $dependency_element_value) {
return FALSE;
}
}
}
return TRUE;
}
$reference_values = [];
foreach ((array) $values as $value) {
$reference_values[] = is_array($value) ? array_shift($value) : $value;
}
if ($options['values_set'] == ConditionalFieldsInterface::CONDITIONAL_FIELDS_DEPENDENCY_VALUES_REGEX) {
foreach ($reference_values as $reference_value) {
if (!preg_match('/' . $options['regex'] . '/', $reference_value)) {
return FALSE;
}
}
return TRUE;
}
if (!empty($options['values']) && is_string($options['values'])) {
$options['values'] = explode("\r\n", $options['values']);
}
switch ($options['values_set']) {
case ConditionalFieldsInterface::CONDITIONAL_FIELDS_DEPENDENCY_VALUES_AND:
$diff = array_diff($options['values'], $reference_values);
return empty($diff);
case ConditionalFieldsInterface::CONDITIONAL_FIELDS_DEPENDENCY_VALUES_OR:
$intersect = array_intersect($options['values'], $reference_values);
return !empty($intersect);
case ConditionalFieldsInterface::CONDITIONAL_FIELDS_DEPENDENCY_VALUES_XOR:
$intersect = array_intersect($options['values'], $reference_values);
return count($intersect) == 1;
case ConditionalFieldsInterface::CONDITIONAL_FIELDS_DEPENDENCY_VALUES_NOT:
$intersect = array_intersect($options['values'], $reference_values);
return empty($intersect);
}
return TRUE;
}
public function elementAddProperty(array $element, $property, $value, $position = 'prepend') {
if (!isset($element[$property])) {
$element[$property] = isset($element['#type']) ? $this->elementInfo
->getInfoProperty($element['#type'], $property, []) : [];
}
if (is_array($value)) {
$value = [
$value,
];
}
if (in_array($value, $element[$property])) {
return $element;
}
switch ($position) {
case 'append':
$element[$property] = array_merge($element[$property], (array) $value);
break;
case 'prepend':
default:
$element[$property] = array_merge((array) $value, $element[$property]);
break;
}
return $element;
}
public function mapStates(array $unmapped_states) {
$states_new = [];
foreach ($unmapped_states as $state_key => $value) {
if (!empty($unmapped_states[$state_key]['AND'])) {
$states_new[$state_key] = $unmapped_states[$state_key]['AND'];
}
if (!empty($unmapped_states[$state_key]['OR'])) {
$or = [];
foreach ($unmapped_states[$state_key]['OR'] as $constraint_key => $constraint_value) {
$or[] = [
$constraint_key => $constraint_value,
];
}
$states_new[$state_key]['1'] = $or;
}
if (!empty($unmapped_states[$state_key]['XOR'])) {
$xor = [
'xor',
];
foreach ($unmapped_states[$state_key]['XOR'] as $constraint_key => $constraint_value) {
$xor[] = [
$constraint_key => $constraint_value,
];
}
$states_new[$state_key]['2'] = $xor;
}
}
return $states_new;
}
public function buildJquerySelectorForField(array $field) {
return conditional_fields_field_selector($field);
}
}