diff --git a/core/includes/form.inc b/core/includes/form.inc index 486600f..210c7bf 100644 --- a/core/includes/form.inc +++ b/core/includes/form.inc @@ -12,7 +12,6 @@ use Drupal\Component\Utility\Xss; use Drupal\Core\Database\Database; use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Form\OptGroup; use Drupal\Core\Render\Element; use Drupal\Core\Template\Attribute; use Symfony\Component\HttpFoundation\RedirectResponse; @@ -119,26 +118,6 @@ function form_set_value($element, $value, FormStateInterface $form_state) { } /** - * Allows PHP array processing of multiple select options with the same value. - * - * Used for form select elements which need to validate HTML option groups - * and multiple options which may return the same value. Associative PHP arrays - * cannot handle these structures, since they share a common key. - * - * @param $array - * The form options array to process. - * - * @return - * An array with all hierarchical elements flattened to a single array. - * - * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0. - * Use \Drupal\Core\Form\OptGroup::flattenOptions(). - */ -function form_options_flatten($array) { - return OptGroup::flattenOptions($array); -} - -/** * Prepares variables for select element templates. * * Default template: select.html.twig. @@ -151,8 +130,6 @@ function form_options_flatten($array) { */ function template_preprocess_select(&$variables) { $element = $variables['element']; - Element::setAttributes($element, array('id', 'name', 'size')); - Element\RenderElement::setAttributes($element, array('form-select')); $variables['attributes'] = $element['#attributes']; $variables['options'] = drupal_render_children($element); @@ -193,133 +170,6 @@ function template_preprocess_option(&$variables) { } /** - * Converts an array of options into HTML, for use in select list form elements. - * - * This function calls itself recursively to obtain the values for each optgroup - * within the list of options and when the function encounters an object with - * an 'options' property inside $element['#options']. - * - * @param array $element - * An associative array containing the following key-value pairs: - * - #multiple: Optional Boolean indicating if the user may select more than - * one item. - * - #options: An associative array of options to render as HTML. Each array - * value can be a string, an array, or an object with an 'option' property: - * - A string or integer key whose value is a translated string is - * interpreted as a single HTML option element. Do not use placeholders - * that sanitize data: doing so will lead to double-escaping. Note that - * the key will be visible in the HTML and could be modified by malicious - * users, so don't put sensitive information in it. - * - A translated string key whose value is an array indicates a group of - * options. The translated string is used as the label attribute for the - * optgroup. Do not use placeholders to sanitize data: doing so will lead - * to double-escaping. The array should contain the options you wish to - * group and should follow the syntax of $element['#options']. - * - If the function encounters a string or integer key whose value is an - * object with an 'option' property, the key is ignored, the contents of - * the option property are interpreted as $element['#options'], and the - * resulting HTML is added to the output. - * - #value: Optional integer, string, or array representing which option(s) - * to pre-select when the list is first displayed. The integer or string - * must match the key of an option in the '#options' list. If '#multiple' is - * TRUE, this can be an array of integers or strings. - * @param array|null $choices - * (optional) Either an associative array of options in the same format as - * $element['#options'] above, or NULL. This parameter is only used internally - * and is not intended to be passed in to the initial function call. - * - * @return string - * An HTML string of options and optgroups for use in a select form element. - */ -function form_select_options($element, $choices = NULL) { - if (!isset($choices)) { - if (empty($element['#options'])) { - return ''; - } - $choices = $element['#options']; - } - // array_key_exists() accommodates the rare event where $element['#value'] is NULL. - // isset() fails in this situation. - $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'])); - $options = ''; - foreach ($choices as $key => $choice) { - if (is_array($choice)) { - $options .= ''; - $options .= form_select_options($element, $choice); - $options .= ''; - } - elseif (is_object($choice) && isset($choice->option)) { - $options .= form_select_options($element, $choice->option); - } - else { - $key = (string) $key; - $empty_choice = $empty_value && $key == '_none'; - if ($value_valid && ((!$value_is_array && (string) $element['#value'] === $key || ($value_is_array && in_array($key, $element['#value']))) || $empty_choice)) { - $selected = ' selected="selected"'; - } - else { - $selected = ''; - } - $options .= ''; - } - } - return SafeMarkup::set($options); -} - -/** - * Returns the indexes of a select element's options matching a given key. - * - * This function is useful if you need to modify the options that are - * already in a form element; for example, to remove choices which are - * not valid because of additional filters imposed by another module. - * One example might be altering the choices in a taxonomy selector. - * To correctly handle the case of a multiple hierarchy taxonomy, - * #options arrays can now hold an array of objects, instead of a - * direct mapping of keys to labels, so that multiple choices in the - * selector can have the same key (and label). This makes it difficult - * to manipulate directly, which is why this helper function exists. - * - * This function does not support optgroups (when the elements of the - * #options array are themselves arrays), and will return FALSE if - * arrays are found. The caller must either flatten/restore or - * manually do their manipulations in this case, since returning the - * index is not sufficient, and supporting this would make the - * "helper" too complicated and cumbersome to be of any help. - * - * As usual with functions that can return array() or FALSE, do not - * forget to use === and !== if needed. - * - * @param $element - * The select element to search. - * @param $key - * The key to look for. - * - * @return - * An array of indexes that match the given $key. Array will be - * empty if no elements were found. FALSE if optgroups were found. - */ -function form_get_options($element, $key) { - $keys = array(); - foreach ($element['#options'] as $index => $choice) { - if (is_array($choice)) { - return FALSE; - } - elseif (is_object($choice)) { - if (isset($choice->option[$key])) { - $keys[] = $index; - } - } - elseif ($index == $key) { - $keys[] = $index; - } - } - return $keys; -} - -/** * Prepares variables for fieldset element templates. * * Default template: fieldset.html.twig. diff --git a/core/lib/Drupal/Core/Form/OptGroup.php b/core/lib/Drupal/Core/Form/OptGroup.php deleted file mode 100644 index 21e9855..0000000 --- a/core/lib/Drupal/Core/Form/OptGroup.php +++ /dev/null @@ -1,58 +0,0 @@ - $value) { - if (is_object($value) && isset($value->option)) { - static::doFlattenOptions($value->option, $options); - } - elseif (is_array($value)) { - static::doFlattenOptions($value, $options); - } - else { - $options[$key] = 1; - } - } - } - -} diff --git a/core/lib/Drupal/Core/Render/Element/Optgroup.php b/core/lib/Drupal/Core/Render/Element/OptGroup.php similarity index 77% rename from core/lib/Drupal/Core/Render/Element/Optgroup.php rename to core/lib/Drupal/Core/Render/Element/OptGroup.php index 12290c7..a46e2a8 100644 --- a/core/lib/Drupal/Core/Render/Element/Optgroup.php +++ b/core/lib/Drupal/Core/Render/Element/OptGroup.php @@ -18,7 +18,7 @@ * @see \Drupal\Core\Render\Element\Select * @see \Drupal\Core\Render\Element\Option */ -class Optgroup extends RenderElement { +class OptGroup extends RenderElement { /** * {@inheritdoc} @@ -27,11 +27,11 @@ public function getInfo() { $class = get_class($this); return array( '#process' => array( - array($class, 'processOptgroup'), + array($class, 'processOptGroup'), array($class, 'processAjaxForm'), ), '#pre_render' => array( - array($class, 'preRenderOptgroup'), + array($class, 'preRenderOptGroup'), ), '#theme' => 'optgroup', '#options' => array(), @@ -61,7 +61,7 @@ public function getInfo() { * * @see _form_validate() */ - public static function processOptgroup(&$element, FormStateInterface $form_state, &$complete_form) { + 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. @@ -104,7 +104,7 @@ public static function processOptgroup(&$element, FormStateInterface $form_state /** * Prepares an option group render element. */ - public static function preRenderOptgroup($element) { + public static function preRenderOptGroup($element) { $element['#attributes']['label'] = $element['#title']; return $element; } @@ -113,7 +113,7 @@ public static function preRenderOptgroup($element) { * Returns a list of options for an option group element. * * This function assumes that the passed element has been processed by - * Optgroup::processOptgroup() prior to calling this function. + * OptGroup::processOptgroup() prior to calling this function. */ public static function getOptions($element) { $options = []; @@ -128,4 +128,30 @@ public static function getOptions($element) { return $options; } + /** + * Iterates over an array building a flat array with duplicate keys removed. + * + * Used for form select elements which need to validate HTML option groups + * and multiple options which may return the same value. Associative PHP + * arrays cannot handle these structures, since they share a common key. + * + * @param array $array + * The form options array to process. + * + * @return array + * An array with all hierarchical elements flattened to a single array. + */ + public static function flattenOptions(array $array) { + $options = []; + foreach ($array as $key => $value) { + if (is_array($value)) { + $options += static::flattenOptions($value); + } + else { + $options[$key] = TRUE; + } + } + return $options; + } + } diff --git a/core/lib/Drupal/Core/Render/Element/Select.php b/core/lib/Drupal/Core/Render/Element/Select.php index be9f5b4..ad3f13d 100644 --- a/core/lib/Drupal/Core/Render/Element/Select.php +++ b/core/lib/Drupal/Core/Render/Element/Select.php @@ -119,7 +119,7 @@ public static function processSelect(&$element, FormStateInterface $form_state, // 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() + // @see \Drupal\Core\Render\Element\OptGroup::processOptgroup() foreach (Element::children($element) as $child) { if ($element[$child]['#type'] == 'optgroup') { $element[$child]['#value'] = $element['#value']; @@ -127,41 +127,37 @@ public static function processSelect(&$element, FormStateInterface $form_state, } } - // Process option groups. - // @TODO Fix weights with mixed optgroups and options. - if (count($element['#options']) > 0) { - $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, - '#value' => $element['#value'], - ); - - // @TODO - Optgroup::processOptgroup($element[$key], $form_state, $complete_form); - - unset($element['#options'][$key]); - } + // Process options and option groups. + $weight = 0; + foreach ($element['#options'] as $key => $choice) { + $key = (string) $key; + + // Maintain order of options as defined in #options, in case both + // options and option groups are defined or the element defines custom + // option sub-elements, but does not define all option sub-elements. + $weight += 0.001; + $element[$key]['#weight'] = $weight; + + // Expand option groups into proper elements. + if (is_array($choice)) { + $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, + '#value' => $element['#value'], + ); + 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); + OptGroup::processOptGroup($element, $form_state, $complete_form); return $element; } @@ -218,11 +214,11 @@ public static function getFlattenedOptions($element) { // @TODO This does not respect weights. foreach (Element::children($element) as $child) { if ($element[$child]['#type'] == 'optgroup') { - $options += Optgroup::getOptions($element[$child]); + $options += OptGroup::getOptions($element[$child]); unset($element[$child]); } } - $options += Optgroup::getOptions($element); + $options += OptGroup::getOptions($element); return $options; } diff --git a/core/modules/entity_reference/src/ConfigurableEntityReferenceItem.php b/core/modules/entity_reference/src/ConfigurableEntityReferenceItem.php index f4fd800..429c489 100644 --- a/core/modules/entity_reference/src/ConfigurableEntityReferenceItem.php +++ b/core/modules/entity_reference/src/ConfigurableEntityReferenceItem.php @@ -11,7 +11,7 @@ use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem; use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Form\OptGroup; +use Drupal\Core\Render\Element\OptGroup; use Drupal\Core\Session\AccountInterface; use Drupal\Core\TypedData\AllowedValuesInterface; use Drupal\Core\TypedData\DataDefinition; diff --git a/core/modules/options/src/Plugin/Field/FieldType/ListItemBase.php b/core/modules/options/src/Plugin/Field/FieldType/ListItemBase.php index af854eb..a934896 100644 --- a/core/modules/options/src/Plugin/Field/FieldType/ListItemBase.php +++ b/core/modules/options/src/Plugin/Field/FieldType/ListItemBase.php @@ -11,7 +11,7 @@ use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemBase; use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Form\OptGroup; +use Drupal\Core\Render\Element\OptGroup; use Drupal\Core\Session\AccountInterface; use Drupal\Core\TypedData\AllowedValuesInterface; diff --git a/core/modules/system/src/Tests/Render/Element/SelectTest.php b/core/modules/system/src/Tests/Render/Element/SelectTest.php new file mode 100644 index 0000000..d2e57a6 --- /dev/null +++ b/core/modules/system/src/Tests/Render/Element/SelectTest.php @@ -0,0 +1,377 @@ +formBuilder = $this->container->get('form_builder'); + + /** @var \Drupal\Core\Extension\ThemeHandler $theme_handler */ + $theme_handler = $this->container->get('theme_handler'); + $theme_handler->enable(['stark']); + $theme_handler->setDefault('stark'); + } + + /** + * Tests the rendering of a select element. + */ + public function testSelect() { + foreach ($this->providerTestSelect() as $test_case) { + $element = $test_case['element']; + // In order to avoid calling FormBuilderInterface::prepareForm() we have + // to supply some default values. + $element += [ + '#id' => 'edit-select', + '#parents' => [], + '#array_parents' => [], + '#tree' => FALSE, + // Avoid the form builder trying to handle the (non-existent) input. + '#input' => FALSE, + ]; + + $output = $test_case['output']; + // The wrapper element never changes, so avoid having to specify it every + // time. + $output = '
' . $output . '
'; + + $form_id = 'select'; + $form_state = new FormState(); + $this->formBuilder->processForm($form_id, $element, $form_state); + $this->assertMarkupEquals($output, drupal_render($element)); + } + } + + /** + * Provides test data for SelectTest::testSelect(). + * + * @return array + * An array of test cases where each test case is an array with the + * following keys: + * - element: The select element to be rendered as part of the test. The + * element may or may not contain additional option or option group + * sub-elements. + * - output: The markup that the rendering of the element is expected to + * yield. + */ + public function providerTestSelect() { + $test_case = []; + $test_cases = []; + + // Test an empty select element. + $test_case['element'] = [ + '#type' => 'select', + '#title' => 'Test select', + ]; + $test_case['output'] = <<Test select + +HTML; + $test_cases[] = $test_case; + + // Test a simple select element. + $test_case['element'] = [ + '#type' => 'select', + '#title' => 'Test select', + '#options' => [ + 'spring' => 'Spring', + 'summer' => 'Summer', + 'fall' => 'Fall', + 'winter' => 'Winter', + ], + '#value' => NULL, + ]; + $test_case['output'] = <<Test select + +HTML; + $test_cases[] = $test_case; + + // Test a select element with a default value. + // Re-use the information from the previous test case. + $test_case['element']['#value'] = 'summer'; + $test_case['output'] = str_replace( + '