language_negotiation_matrix-1.0.0-beta2/src/Element/Mapping.php
src/Element/Mapping.php
<?php
namespace Drupal\language_negotiation_matrix\Element;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Element\FormElement;
/**
* Provides a mapping element.
*
* @FormElement("mapping")
*/
class Mapping extends FormElement {
/**
* Require all.
*/
const REQUIRED_ALL = 'all';
/**
* Option description delimiter.
*
* @var string
*/
const DESCRIPTION_DELIMITER = ' -- ';
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return [
'#input' => TRUE,
'#process' => [
[$class, 'processMapping'],
[$class, 'processAjaxForm'],
],
'#theme_wrappers' => ['form_element'],
'#filter' => TRUE,
'#required' => FALSE,
'#source' => [],
'#source__description_display' => 'description',
'#destination' => [],
'#arrow' => '→',
];
}
/**
* Processes a likert scale form element.
*/
public static function processMapping(&$element, FormStateInterface $form_state, &$complete_form) {
// Set translated default properties.
$element += [
'#source__title' => t('Source'),
'#destination__title' => t('Destination'),
'#arrow' => '→',
];
$arrow = htmlentities($element['#arrow']);
// Process sources.
$sources = [];
foreach ($element['#source'] as $source_key => $source) {
$source = (string) $source;
if (!static::hasOptionDescription($source)) {
$source_description_property_name = NULL;
$source_title = $source;
$source_description = '';
}
else {
$source_description_property_name = ($element['#source__description_display'] === 'help') ? 'help' : 'description';
[$source_title, $source_description] = static::splitOption($source);
}
$sources[$source_key] = [
'description_property_name' => $source_description_property_name,
'title' => $source_title,
'description' => $source_description,
];
}
// Setup destination__type depending if #destination is defined.
if (empty($element['#destination__type'])) {
$element['#destination__type'] = (empty($element['#destination'])) ? 'textfield' : 'select';
}
// Set base destination element.
$destination_element_base = [
'#title_display' => 'invisible',
'#required' => ($element['#required'] === static::REQUIRED_ALL) ? TRUE : FALSE,
'#error_no_message' => ($element['#required'] !== static::REQUIRED_ALL) ? TRUE : FALSE,
];
// Get base #destination__* properties.
foreach ($element as $element_key => $element_value) {
if (strpos($element_key, '#destination__') === 0 && !in_array($element_key, ['#destination__title'])) {
$destination_element_base[str_replace('#destination__', '#', $element_key)] = $element_value;
}
}
// Build header.
$header = [
['data' => ['#markup' => $element['#source__title'] . ' ' . $arrow]],
['data' => ['#markup' => $element['#destination__title']]],
];
// Build rows.
$rows = [];
foreach ($sources as $source_key => $source) {
$default_value = (isset($element['#default_value'][$source_key])) ? $element['#default_value'][$source_key] : NULL;
// Source element.
$source_element = ['data' => []];
$source_element['data']['title'] = ['#markup' => $source['title']];
if ($source['description_property_name'] === 'help') {
$source_element['data']['help'] = [
'#type' => 'details',
'#description' => $source['description'],
'#title' => $source['title'],
];
}
$source_element['data']['arrow'] = ['#markup' => $arrow, '#prefix' => ' '];
if ($source['description_property_name'] === 'description') {
$source_element['data']['description'] = [
'#type' => 'container',
'#markup' => $source['description'],
'#attributes' => ['class' => ['description']],
];
}
// Destination element.
$destination_element = $destination_element_base + [
'#title' => $source['title'],
'#required' => $element['#required'],
'#default_value' => $default_value,
];
// Apply #parents to destination element.
if (isset($element['#parents'])) {
$destination_element['#parents'] = array_merge($element['#parents'], [$source_key]);
}
switch ($element['#destination__type']) {
case 'select':
$destination_element += [
'#empty_option' => t('- Select -'),
'#options' => $element['#destination'],
];
break;
}
// Add row.
$rows[$source_key] = [
'source' => $source_element,
$source_key => $destination_element,
];
}
$element['table'] = [
'#tree' => TRUE,
'#type' => 'table',
'#header' => $header,
'#attributes' => [
'class' => ['mapping-table'],
],
] + $rows;
// Build table element with selected properties.
$properties = [
'#states',
'#sticky',
];
$element['table'] += array_intersect_key($element, array_combine($properties, $properties));
// Add validate callback.
$element += ['#element_validate' => []];
array_unshift($element['#element_validate'], [get_called_class(), 'validateMapping']);
if (!empty($element['#states'])) {
static::processStates($element, '#wrapper_attributes');
}
$element['#attached']['library'][] = 'language_negotiation_matrix/language_negotiation_matrix.element.mapping';
return $element;
}
/**
* Validates a mapping element.
*/
public static function validateMapping(&$element, FormStateInterface $form_state, &$complete_form) {
$value = NestedArray::getValue($form_state->getValues(), $element['#parents']);
// Filter values.
if ($element['#filter']) {
$value = array_filter($value);
}
// Note: Not validating REQUIRED_ALL because each destination element is
// already required.
if (Element::isVisibleElement($element)
&& $element['#required']
&& $element['#required'] !== static::REQUIRED_ALL
&& empty($value)) {
static::setRequiredError($element, $form_state);
}
$element['#value'] = $value;
$form_state->setValueForElement($element, $value);
}
/**
* Set form state required error for a specified element.
*
* @param array $element
* An element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param string $title
* OPTIONAL. Required error title.
*/
public static function setRequiredError(array $element, FormStateInterface $form_state, $title = NULL) {
if (isset($element['#required_error'])) {
$form_state->setError($element, $element['#required_error']);
}
elseif ($title) {
$form_state->setError($element, t('@name field is required.', ['@name' => $title]));
}
elseif (isset($element['#title'])) {
$form_state->setError($element, t('@name field is required.', ['@name' => $element['#title']]));
}
else {
$form_state->setError($element);
}
}
/**
* Adds JavaScript to change the state of an element based on another element.
*
* @param array $elements
* A renderable array element having a #states property as described above.
* @param string $key
* The element property to add the states attribute to.
*
* @see \Drupal\Core\Form\FormHelper::processStates
*/
public static function processStates(array &$elements, $key = '#attributes') {
if (empty($elements['#states'])) {
return;
}
$elements['#attached']['library'][] = 'core/drupal.states';
$elements[$key]['data-drupal-states'] = Json::encode($elements['#states']);
// Make sure to include target class for this container.
if (empty($elements[$key]['class']) || !static::inArray(['js-form-item', 'js-form-submit', 'js-form-wrapper'], $elements[$key]['class'])) {
$elements[$key]['class'][] = 'js-form-item';
}
}
/**
* Determine if any values are in an array.
*
* @param array $needles
* The searched values.
* @param array $haystack
* The array.
*
* @return bool
* TRUE if any values are in an array.
*
* @see http://stackoverflow.com/questions/7542694/in-array-multiple-values
*/
public static function inArray(array $needles, array $haystack) {
return !!array_intersect($needles, $haystack);
}
/**
* Determine if option text includes a description.
*
* @param string $text
* Option text.
*
* @return bool
* TRUE option text includes a description.
*/
public static function hasOptionDescription($text) {
return (strpos($text, static::DESCRIPTION_DELIMITER) !== FALSE) ? TRUE : FALSE;
}
/**
* Split option text into an array containing an option's text and description.
*
* @param string $text
* Option text.
*
* @return array
* An array containing an option's text and description.
*/
public static function splitOption($text) {
return explode(static::DESCRIPTION_DELIMITER, $text);
}
}
