htools-8.x-1.x-dev/modules/options_element/src/Element/Options.php
modules/options_element/src/Element/Options.php
<?php
/**
* @file
* Contains \Drupal\options_element\Element\Options.
*/
namespace Drupal\options_element\Element;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Element\FormElement;
use Drupal\options_element\OptionsUtilities;
/**
* Defines the 'options' form element type.
*
* The 'options' form element type is useful when collecting a series of
* values in a list. The values within the list may optionally have unique
* keys, such as that in an array structure. In addition, a default choice
* (or several default choices) may be selected by the user.
*
* @code
* $element['options'] = array(
* '#type' => 'options',
* '#limit' => 20,
* '#optgroups' => FALSE,
* '#multiple' => FALSE,
* '#options' => array(
* 'foo' => 'foo',
* 'bar' => 'bar',
* 'baz' => 'baz',
* ),
* '#default_value' => 'foo'
* '#key_type' => 'associative',
* );
* @endcode
*
* Properties for the 'options' element include:
* - limit: The maximum number of options that can be added to a list. Defaults
* to 100.
* - optgroups: If nesting of options is supported, up to one level. This is
* used when building a select HTML element that uses optgroups. Defaults to
* FALSE.
* - multiple: Affects the number of default values that may be selected.
* - default_value: The key(s) for the options that are currently selected. If
* #multiple is TRUE then, the default value is an array, otherwise it is a
* string.
* - options: An array of options currently within the list.
* - key_type: The method by which keys are determined for each value in the
* option list. Available options include:
* - mixed: Each value is not given any ID automatically, but any manually
* specified keys will be retained. This most emulates the existing
* conventions within Drupal, where keys are optional but allowed.
* - numeric: Each value is automatically given a unique numeric ID. This can
* be useful when wanting duplicate values in a list and not have to bother
* the end-user for keys.
* - associative: Keys are automatically mapped from the user-entered values.
* This is equivalent to making key|value pairs, but both the key and value
* are the same. Each key must be unique.
* - custom: Keys are manually entered by the end user. A second set of
* textfields are presented to let the user provide keys as well as values.
* - none: No keys are specified at all. This effectively creates numeric keys
* but unlike numeric keys, the keys are renumbered if the options in the
* list are rearranged.
* - key_type_toggle: If specified, a checkbox will be added that allows the
* user to toggle between the current key type and the "custom" key type,
* letting them customize the keys as desired. This option has no effect with
* the "none" key type.
* - key_type_toggled: Determine if the toggle checkbox is set or not by
* default.
* - default_value_allowed: Indicates whether the end user should be able to
* modify the default value when editing the options list. Defaults to TRUE.
* - default_value_pattern: If allowing dynamic default value keys, such as a
* token, specify a regular expression pattern that will also be allowed as
* a default value. Include pattern delimiters. Defaults to an empty string.
*
* @code
* $element['options'] = array(
* '#type' => 'options',
* '#key_type' => 'associative',
* '#key_type_toggle' => t('Custom keys'),
* '#key_type_toggled' => TRUE,
* );
* @endcode
*
* @FormElement("options")
*/
class Options extends FormElement {
public function getInfo() {
$class = get_class($this);
return array(
'#input' => TRUE,
'#limit' => NULL,
'#optgroups' => TRUE,
'#multiple' => FALSE,
'#options' => array(),
'#options_readonly' => FALSE,
'#key_type' => 'mixed',
'#key_type_toggle' => NULL,
'#key_type_toggled' => FALSE,
'#default_value_allowed' => TRUE,
'#default_value_pattern' => '',
'#process' => array(
array($class, 'processOptions'),
),
'#pre_render' => array(
array($class, 'preRenderOptions'),
),
'#element_validate' => array(
array($class, 'validateOptions'),
),
'#theme_wrappers' => array('form_element'),
);
}
/**
* Expand the "options" form element type.
*
* The "options" type is simply an enhanced textarea that makes it easier to
* create key|value pairs and put items into optgroups.
* @param array $element
* @param \Drupal\Core\Form\FormStateInterface $form_state
* @param array $complete_form
* @return array
*/
public static function processOptions(&$element, FormStateInterface $form_state, &$complete_form) {
$element['#options'] = isset($element['#options']) ? $element['#options'] : array();
$element['#multiple'] = isset($element['#multiple']) ? $element['#multiple'] : FALSE;
$element['#tree'] = TRUE;
$element['#theme'] = 'options';
$element['#attached']['library'][] = 'options_element/options_element';
// Add the key type toggle checkbox.
if (!isset($element['custom_keys']) && $element['#key_type'] != 'custom' && !empty($element['#key_type_toggle'])) {
$element['custom_keys'] = array(
'#title' => is_string($element['#key_type_toggle']) ? $element['#key_type_toggle'] : t('Customize keys'),
'#type' => 'checkbox',
'#default_value' => $element['#key_type_toggled'],
'#attributes' => array('class' => array('key-type-toggle')),
'#description' => t('Customizing the keys will allow you to save one value internally while showing a different option to the user.'),
);
}
// Add the multiple value toggle checkbox.
if (!isset($element['multiple']) && !empty($element['#multiple_toggle'])) {
$element['multiple'] = array(
'#title' => is_string($element['#multiple_toggle']) ? $element['#multiple_toggle'] : t('Allow multiple values'),
'#type' => 'checkbox',
'#default_value' => !empty($element['#multiple']),
'#attributes' => array('class' => array('multiple-toggle')),
'#description' => t('Multiple values will let users select multiple items in this list.'),
);
}
// If the element had a custom interface for toggling whether or not multiple
// values are accepted, make sure that form_type_options_value() knows to use
// it.
if (isset($element['multiple']) && empty($element['#multiple_toggle'])) {
$element['#multiple_toggle'] = TRUE;
}
// Add the main textarea for adding options.
if (!isset($element['options'])) {
$element['options_field'] = array(
'#type' => 'textarea',
'#resizable' => TRUE,
'#cols' => 60,
'#rows' => 5,
'#required' => isset($element['#required']) ? $element['#required'] : FALSE,
'#description' => t('List options one option per line.'),
'#attributes' => $element['#options_readonly'] ? array('readonly' => 'readonly') : array(),
'#wysiwyg' => FALSE, // Prevent CKeditor from trying to hijack.
);
// If validation fails, reload the user's text even if it's not valid.
if (isset($element['#value']['options_text'])) {
$element['options_field']['#value'] = $element['#value']['options_text'];
}
// Most of the time, we'll be converting the options array into the text.
else {
$element['options_field']['#value'] = isset($element['#options']) ? OptionsUtilities::optionsToText($element['#options'], $element['#key_type']) : '';
}
if ($element['#key_type'] == 'mixed' || $element['#key_type'] == 'numeric' || $element['#key_type'] == 'custom') {
$element['options_field']['#description'] .= ' ' . t('Key-value pairs may be specified by separating each option with pipes, such as <em>key|value</em>.');
}
elseif ($element['#key_type_toggle']) {
$element['options_field']['#description'] .= ' ' . t('If the %toggle field is checked, key-value pairs may be specified by separating each option with pipes, such as <em>key|value</em>.', array('%toggle' => $element['custom_keys']['#title']));
}
if ($element['#key_type'] == 'numeric') {
$element['options_field']['#description'] .= ' ' . t('This field requires all specified keys to be integers.');
}
}
// Add the field for storing default values.
if ($element['#default_value_allowed'] && !isset($element['default_value_field'])) {
$element['default_value_field'] = array(
'#title' => t('Default value'),
'#type' => 'textfield',
'#size' => 60,
'#maxlength' => 1024,
'#value' => isset($element['#default_value']) ? ($element['#multiple'] ? implode(', ', (array) $element['#default_value']) : $element['#default_value']) : '',
'#description' => t('Specify the keys that should be selected by default.'),
);
if ($element['#multiple']) {
$element['default_value_field']['#description'] .= ' ' . t('Multiple default values may be specified by separating keys with commas.');
}
}
// Add the field for storing a default value pattern.
if ($element['#default_value_pattern']) {
$element['default_value_pattern'] = array(
'#type' => 'hidden',
'#value' => $element['#default_value_pattern'],
'#attributes' => array('class' => array('default-value-pattern')),
);
}
// Remove properties that will confuse the FAPI.
unset($element['#options']);
return $element;
}
/**
* Validate the "options" form element type.
*
* @param array $element
* @param \Drupal\Core\Form\FormStateInterface $form_state
* @param array $complete_form
*/
public static function validateOptions(&$element, FormStateInterface $form_state, &$complete_form) {
// Even though we already have the converted options in #value['options'], run
// the conversion again to check for duplicates in the user-defined list.
$duplicates = array();
$options = OptionsUtilities::optionsFromText($element['#value']['options_text'], $element['#key_type'], empty($element['#optgroups']), $duplicates);
// Check if a key is used multiple times.
if (count($duplicates) == 1) {
$form_state->setError($element, t('The key %key has been used multiple times. Each key must be unique to display properly.', array('%key' => reset($duplicates))));
}
elseif (!empty($duplicates)) {
array_walk($duplicates, 'check_plain');
$duplicate_list = [
'#theme' => 'item_list',
'#items' => $duplicates
];
$form_state->setError($element, t('The following keys have been used multiple times. Each key must be unique to display properly.') . $duplicate_list);
}
// Add the list of duplicates to the page so that we can highlight the fields.
if (!empty($duplicates)) {
$element['#attached']['drupalSettings']['optionsElement']['errors'] = array_combine($duplicates, $duplicates);
}
// Check if no options are specified.
if (empty($options) && $element['#required']) {
$form_state->setError($element, t('At least one option must be specified.'));
}
// Check for numeric keys if needed.
if ($element['#key_type'] == 'numeric') {
foreach ($options as $key => $value) {
if (!is_int($key)) {
$form_state->setError($element, t('The keys for the %title field must be integers.', array('%title' => $element['#title'])));
break;
}
}
}
// Check that the limit of options has not been exceeded.
if (!empty($element['#limit'])) {
$count = 0;
foreach ($options as $value) {
if (is_array($value)) {
$count += count($value);
}
else {
$count++;
}
}
if ($count > $element['#limit']) {
$form_state->setError($element, t('The %title field supports a maximum of @count options. Please reduce the number of options.', array('%title' => $element['#title'], '@count' => $element['#limit'])));
}
}
}
/**
* This function adjusts the value of the element from a text value to an array.
*
* @param array $element
* @param array|FALSE $input
* @param \Drupal\Core\Form\FormStateInterface $form_state
* @return array
*/
public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
if ($input === FALSE) {
return array(
'options' => isset($element['#options']) ? $element['#options'] : array(),
'default_value' => isset($element['#default_value']) ? $element['#default_value'] : '',
);
}
else {
// Convert text to an array of options.
$duplicates = array();
$options = OptionsUtilities::optionsFromText($input['options_field'], $element['#key_type'], empty($element['#optgroups']), $duplicates);
// Convert default value.
if (isset($input['default_value_field'])) {
// If the element supports toggling whether or not it will accept
// multiple values, use the value that was passed in via $input (keeping
// in mind that this value may not be set, if a checkbox was used to
// configure it). Otherwise, use the current setting stored with the
// element itself.
$multiple = !empty($element['#multiple_toggle']) ? !empty($input['multiple']) : !empty($element['#multiple']);
if ($multiple) {
$default_value = array();
$default_items = explode(',', $input['default_value_field']);
foreach ($default_items as $key) {
$key = trim($key);
$value = OptionsUtilities::optionsSearch($key, $options, $element['#default_value_pattern']);
if (!is_null($value)) {
$default_value[] = $value;
}
}
}
else {
$default_value = OptionsUtilities::optionsSearch(trim($input['default_value_field']), $options, $element['#default_value_pattern']);
}
}
else {
$default_value = NULL;
}
$return = array(
'options' => $options,
'default_value' => $default_value,
'options_text' => $input['options_field'],
);
if (isset($input['default_value_field'])) {
$return['default_value_text'] = $input['default_value_field'];
}
return $return;
}
}
/**
* Prepares a #type 'options' render element for input.html.twig.
*
* @param array $element
* An associative array containing the properties of the element.
* Properties used: #title, #value, #return_value, #description, #required,
* #attributes, #checked.
*
* @return array
* The $element with prepared variables ready for options.html.twig.
*/
public static function preRenderOptions($element) {
$element['#attributes']['type'] = 'options';
Element::setAttributes($element, array('id', 'name', '#return_value' => 'value'));
$element['#attributes']['class'][] = 'form-options';
$classes = &$element['#attributes']['class'];
$classes[] = 'options-key-type-'. $element['#key_type'];
if ($element['#key_type_toggled']) {
$classes[] = 'options-key-custom';
}
if (isset($element['#optgroups']) && $element['#optgroups']) {
$classes[] = 'options-optgroups';
}
if (isset($element['#multiple']) && $element['#multiple']) {
$classes[] = 'options-multiple';
}
// Replace the error class from wrapper div, which doesn't display well with
// complex elements like Options Element.
if ($key = array_search('error', $classes, TRUE)) {
$classes[$key] = 'options-error';
}
return $element;
}
}