diff --git a/core/includes/form.inc b/core/includes/form.inc index 360155e..6e0d57b 100644 --- a/core/includes/form.inc +++ b/core/includes/form.inc @@ -191,10 +191,6 @@ function form_options_flatten($array) { * * Default template: select.html.twig. * - * It is possible to group options together; to do this, change the format of - * $options to an associative array in which the keys are group labels, and the - * values are associative arrays in the normal $options format. - * * @param $variables * An associative array containing: * - element: An associative array containing the properties of the element. @@ -207,7 +203,41 @@ function template_preprocess_select(&$variables) { Element\RenderElement::setAttributes($element, array('form-select')); $variables['attributes'] = $element['#attributes']; - $variables['options'] = form_select_options($element); + $variables['options'] = drupal_render_children($element); +} + +/** + * Prepares variables for option group element templates. + * + * Default template: optgroup.html.twig. + * + * @param $variables + * An associative array containing: + * - element: An associative array containing the properties of the element. + * Properties used: #title, #value, #options, #multiple, #attributes. + */ +function template_preprocess_optgroup(&$variables) { + $element = $variables['element']; + + $variables['attributes'] = $element['#attributes']; + $variables['options'] = drupal_render_children($element); +} + +/** + * Prepares variables for option element templates. + * + * Default template: option.html.twig. + * + * @param $variables + * An associative array containing: + * - element: An associative array containing the properties of the element. + * Properties used: #title, #value, #attributes. + */ +function template_preprocess_option(&$variables) { + $element = $variables['element']; + + $variables['attributes'] = $element['#attributes']; + $variables['label'] = $element['#title']; } /** diff --git a/core/includes/theme.inc b/core/includes/theme.inc index b349918..8b668b3 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -2396,6 +2396,14 @@ function drupal_common_theme() { 'render element' => 'element', 'template' => 'select', ), + 'option' => array( + 'render element' => 'element', + 'template' => 'option', + ), + 'optgroup' => array( + 'render element' => 'element', + 'template' => 'optgroup', + ), 'fieldset' => array( 'render element' => 'element', 'template' => 'fieldset', diff --git a/core/lib/Drupal/Core/Render/Element/Optgroup.php b/core/lib/Drupal/Core/Render/Element/Optgroup.php new file mode 100644 index 0000000..04b963c --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Optgroup.php @@ -0,0 +1,112 @@ + array( + array($class, 'processOptgroup'), + array($class, 'processAjaxForm'), + ), + '#pre_render' => array( + array($class, 'preRenderOptgroup'), + ), + '#theme' => 'optgroup', + '#options' => array(), + ); + } + + /** + * Processes an option group form element. + * + * This expands the #options property of option groups into proper option + * elements. + * + * @param array $element + * The form element to process. Properties used: + * - #options: (optional) An array of options for this option group where + * the keys are the option values and the values are the respective + * labels. Instead of specifying the options in this way, respective + * option elements can be created as child elements of an option group + * element. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * @param array $complete_form + * The complete form structure. + * + * @return array + * The processed element. + * + * @see _form_validate() + */ + public static function processOptgroup(&$element, FormStateInterface $form_state, &$complete_form) { + // @TODO This check is taken over from \Drupal\Core\Render\Element\Radios + // and \Drupal\Core\Render\Element\Checkboxes, but I do not understand why + // it is necessary. + if (count($element['#options']) > 0) { + // @TODO This is ported from form_select_options(). But instead of + // checking #value, #default_value should be used here and below, right? + $value_valid = isset($element['#value']) || array_key_exists('#value', $element); + $value_is_array = $value_valid && is_array($element['#value']); + // Check if the element is multiple select and no value has been selected. + $empty_value = (empty($element['#value']) && !empty($element['#multiple'])); + + $weight = 0; + foreach ($element['#options'] as $key => $choice) { + $key = (string) $key; + + // Maintain order of options as defined in #options, in case the element + // defines custom option sub-elements, but does not define all option + // sub-elements. + $weight += 0.001; + + $empty_choice = $empty_value && $key == '_none'; + + $element += array($key => array()); + $element[$key] += array( + '#type' => 'option', + '#title' => $choice, + // The key is sanitized in Drupal\Core\Template\Attribute during output + // from the theme function. + '#return_value' => $key, + '#selected' => $value_valid && ((!$value_is_array && (string) $element['#value'] === $key || ($value_is_array && in_array($key, $element['#value']))) || $empty_choice), + '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL, + '#weight' => $weight, + ); + } + } + + return $element; + } + + /** + * Prepares an option group render element. + */ + public static function preRenderOptgroup($element) { + $element['#attributes']['label'] = $element['#title']; + return $element; + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/Option.php b/core/lib/Drupal/Core/Render/Element/Option.php new file mode 100644 index 0000000..036a10e --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Option.php @@ -0,0 +1,50 @@ + array( + array($class, 'processAjaxForm'), + ), + '#pre_render' => array( + array($class, 'preRenderOption'), + ), + '#theme' => 'option', + '#selected' => FALSE, + ); + } + + /** + * Prepares an option render element. + */ + public static function preRenderOption($element) { + $element['#attributes']['value'] = $element['#return_value']; + if ($element['#selected']) { + $element['#attributes']['selected'] = 'selected'; + } + return $element; + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/Select.php b/core/lib/Drupal/Core/Render/Element/Select.php index 70f14d6..beae6bc 100644 --- a/core/lib/Drupal/Core/Render/Element/Select.php +++ b/core/lib/Drupal/Core/Render/Element/Select.php @@ -17,6 +17,9 @@ * a select element, including behavior if #required is TRUE or FALSE. * * @FormElement("select") + * + * @see \Drupal\Core\Render\Element\Option + * @see \Drupal\Core\Render\Element\Optgroup */ class Select extends FormElement { @@ -113,6 +116,59 @@ public static function processSelect(&$element, FormStateInterface $form_state, $element['#options'] = $empty_option + $element['#options']; } } + + // If there are option group elements, pass the '#value' and '#multiple' + // keys down so they can properly set the selected option. + // @see \Drupal\Core\Render\Element\Optgroup::processOptgroup() + foreach (Element::children($element) as $child) { + if ($element[$child]['#type'] == 'optgroup') { + $element[$child]['#value'] = $element['#value']; + $element[$child]['#multiple'] = $element['#multiple']; + } + } + + // Process option groups. + // @TODO Fix weights with mixed optgroups and options. + if (count($element['#options']) > 0) { + // @TODO This is ported from form_select_options(). But instead of + // checking #value, #default_value should be used here and below, right? + $value_valid = isset($element['#value']) || array_key_exists('#value', $element); + $value_is_array = $value_valid && is_array($element['#value']); + // Check if the element is multiple select and no value has been selected. + $empty_value = (empty($element['#value']) && !empty($element['#multiple'])); + + $weight = 0; + foreach ($element['#options'] as $key => $choice) { + $key = (string) $key; + + if (is_array($choice)) { + // Maintain order of options as defined in #options, in case the element + // defines custom option sub-elements, but does not define all option + // sub-elements. + $weight += 0.001; + + $element += array($key => array()); + $element[$key] += array( + '#type' => 'optgroup', + '#title' => $key, + // The key is sanitized in Drupal\Core\Template\Attribute during output + // from the theme function. + '#options' => $choice, + '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL, + '#weight' => $weight, + ); + + // @TODO + Optgroup::processOptgroup($element[$key], $form_state, $complete_form); + + unset($element['#options'][$key]); + } + } + } + + // Process the '#options' element in the same way that option groups do. + Optgroup::processOptgroup($element, $form_state, $complete_form); + return $element; } diff --git a/core/modules/system/templates/optgroup.html.twig b/core/modules/system/templates/optgroup.html.twig new file mode 100644 index 0000000..1efe368 --- /dev/null +++ b/core/modules/system/templates/optgroup.html.twig @@ -0,0 +1,13 @@ +{# +/** + * @file + * Default theme implementation for an optgroup element. + * + * Available variables: + * - attributes: HTML attributes for the optgroup tag. + * - options: The option element children. + * + * @ingroup themeable + */ +#} +{{ options }} diff --git a/core/modules/system/templates/option.html.twig b/core/modules/system/templates/option.html.twig new file mode 100644 index 0000000..d795da9 --- /dev/null +++ b/core/modules/system/templates/option.html.twig @@ -0,0 +1,13 @@ +{# +/** + * @file + * Default theme implementation for an option element. + * + * Available variables: + * - attributes: HTML attributes for the option tag. + * - label: The option label. + * + * @ingroup themeable + */ +#} +{{ label }}