diff --git a/core/includes/ajax.inc b/core/includes/ajax.inc index 3b5c2779b133e696bbfd193444c23e79efc7ed86..df3ce78a5627f916f2412ce82c47f14fa2890032 100644 --- a/core/includes/ajax.inc +++ b/core/includes/ajax.inc @@ -163,10 +163,10 @@ /** * Form element processing handler for the #ajax form property. * - * @deprecated Use \Drupal\Core\Render\Element\FormElement::processAjaxForm(). + * @deprecated Use \Drupal\Core\Render\Element\RenderElement::processAjaxForm(). */ -function ajax_process_form($element, FormStateInterface $form_state, &$complete_form) { - return Element\FormElement::processAjaxForm($element, $form_state, $complete_form); +function ajax_process_form(&$element, FormStateInterface $form_state, &$complete_form) { + return Element\RenderElement::processAjaxForm($element, $form_state, $complete_form); } /** diff --git a/core/includes/common.inc b/core/includes/common.inc index b9f2d8f0db32ad2c390d40f0da75200317296389..3b07dd7a8a2a1256fad3040665b46bdad9fe1bed 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -1062,8 +1062,9 @@ function drupal_add_html_head_link($attributes, $header = FALSE) { * - 'preprocess': If TRUE and CSS aggregation/compression is enabled, the * styles will be aggregated and compressed. Defaults to TRUE. * - 'browsers': An array containing information specifying which browsers - * should load the CSS item. See drupal_pre_render_conditional_comments() - * for details. + * should load the CSS item. See + * \Drupal\Core\Render\Element\HtmlTag::preRenderConditionalComments() for + * details. * * @return * An array of queued cascading stylesheets. @@ -1275,72 +1276,6 @@ function drupal_sort_css_js($a, $b) { } } -/** - * Pre-render callback: Adds the elements needed for CSS tags to be rendered. - * - * For production websites, LINK tags are preferable to STYLE tags with @import - * statements, because: - * - They are the standard tag intended for linking to a resource. - * - On Firefox 2 and perhaps other browsers, CSS files included with @import - * statements don't get saved when saving the complete web page for offline - * use: http://drupal.org/node/145218. - * - On IE, if only LINK tags and no @import statements are used, all the CSS - * files are downloaded in parallel, resulting in faster page load, but if - * @import statements are used and span across multiple STYLE tags, all the - * ones from one STYLE tag must be downloaded before downloading begins for - * the next STYLE tag. Furthermore, IE7 does not support media declaration on - * the @import statement, so multiple STYLE tags must be used when different - * files are for different media types. Non-IE browsers always download in - * parallel, so this is an IE-specific performance quirk: - * http://www.stevesouders.com/blog/2009/04/09/dont-use-import/. - * - * However, IE has an annoying limit of 31 total CSS inclusion tags - * (http://drupal.org/node/228818) and LINK tags are limited to one file per - * tag, whereas STYLE tags can contain multiple @import statements allowing - * multiple files to be loaded per tag. When CSS aggregation is disabled, a - * Drupal site can easily have more than 31 CSS files that need to be loaded, so - * using LINK tags exclusively would result in a site that would display - * incorrectly in IE. Depending on different needs, different strategies can be - * employed to decide when to use LINK tags and when to use STYLE tags. - * - * The strategy employed by this function is to use LINK tags for all aggregate - * files and for all files that cannot be aggregated (e.g., if 'preprocess' is - * set to FALSE or the type is 'external'), and to use STYLE tags for groups - * of files that could be aggregated together but aren't (e.g., if the site-wide - * aggregation setting is disabled). This results in all LINK tags when - * aggregation is enabled, a guarantee that as many or only slightly more tags - * are used with aggregation disabled than enabled (so that if the limit were to - * be crossed with aggregation enabled, the site developer would also notice the - * problem while aggregation is disabled), and an easy way for a developer to - * view HTML source while aggregation is disabled and know what files will be - * aggregated together when aggregation becomes enabled. - * - * This function evaluates the aggregation enabled/disabled condition on a group - * by group basis by testing whether an aggregate file has been made for the - * group rather than by testing the site-wide aggregation setting. This allows - * this function to work correctly even if modules have implemented custom - * logic for grouping and aggregating files. - * - * @param $element - * A render array containing: - * - '#items': The CSS items as returned by _drupal_add_css() and altered by - * drupal_get_css(). - * - * @return - * A render array that will render to a string of XHTML CSS tags. - * - * @see drupal_get_css() - */ -function drupal_pre_render_styles($elements) { - $css_assets = $elements['#items']; - - // Aggregate the CSS if necessary, but only during normal site operation. - if (!defined('MAINTENANCE_MODE') && \Drupal::config('system.performance')->get('css.preprocess')) { - $css_assets = \Drupal::service('asset.css.collection_optimizer')->optimize($css_assets); - } - return \Drupal::service('asset.css.collection_renderer')->render($css_assets); -} - /** * Deletes old cached CSS files. * @@ -1670,7 +1605,8 @@ function drupal_clean_id_identifier($id) { * 'preprocess' option was set to FALSE. * - browsers: An array containing information specifying which browsers * should load the JavaScript item. See - * drupal_pre_render_conditional_comments() for details. + * \Drupal\Core\Render\Element\HtmlTag::preRenderConditionalComments() for + * details. * * @return * The current array of JavaScript files, settings, and in-line code, @@ -1990,41 +1926,6 @@ function drupal_merge_attached(array $a, array $b) { return NestedArray::mergeDeep($a, $b); } -/** - * #pre_render callback to add the elements needed for JavaScript tags to be rendered. - * - * This function evaluates the aggregation enabled/disabled condition on a group - * by group basis by testing whether an aggregate file has been made for the - * group rather than by testing the site-wide aggregation setting. This allows - * this function to work correctly even if modules have implemented custom - * logic for grouping and aggregating files. - * - * @param $element - * A render array containing: - * - #items: The JavaScript items as returned by _drupal_add_js() and - * altered by drupal_get_js(). - * - #group_callback: A function to call to group #items. Following - * this function, #aggregate_callback is called to aggregate items within - * the same group into a single file. - * - #aggregate_callback: A function to call to aggregate the items within - * the groups arranged by the #group_callback function. - * - * @return - * A render array that will render to a string of JavaScript tags. - * - * @see drupal_get_js() - */ -function drupal_pre_render_scripts($elements) { - $js_assets = $elements['#items']; - - // Aggregate the JavaScript if necessary, but only during normal site - // operation. - if (!defined('MAINTENANCE_MODE') && \Drupal::config('system.performance')->get('js.preprocess')) { - $js_assets = \Drupal::service('asset.js.collection_optimizer')->optimize($js_assets); - } - return \Drupal::service('asset.js.collection_renderer')->render($js_assets); -} - /** * Adds attachments to a render() structure. * @@ -2562,137 +2463,6 @@ function drupal_set_page_content($content = NULL) { } } -/** - * Pre-render callback: Renders #browsers into #prefix and #suffix. - * - * @param $elements - * A render array with a '#browsers' property. The '#browsers' property can - * contain any or all of the following keys: - * - 'IE': If FALSE, the element is not rendered by Internet Explorer. If - * TRUE, the element is rendered by Internet Explorer. Can also be a string - * containing an expression for Internet Explorer to evaluate as part of a - * conditional comment. For example, this can be set to 'lt IE 7' for the - * element to be rendered in Internet Explorer 6, but not in Internet - * Explorer 7 or higher. Defaults to TRUE. - * - '!IE': If FALSE, the element is not rendered by browsers other than - * Internet Explorer. If TRUE, the element is rendered by those browsers. - * Defaults to TRUE. - * Examples: - * - To render an element in all browsers, '#browsers' can be left out or set - * to array('IE' => TRUE, '!IE' => TRUE). - * - To render an element in Internet Explorer only, '#browsers' can be set - * to array('!IE' => FALSE). - * - To render an element in Internet Explorer 6 only, '#browsers' can be set - * to array('IE' => 'lt IE 7', '!IE' => FALSE). - * - To render an element in Internet Explorer 8 and higher and in all other - * browsers, '#browsers' can be set to array('IE' => 'gte IE 8'). - * - * @return - * The passed-in element with markup for conditional comments potentially - * added to '#prefix' and '#suffix'. - */ -function drupal_pre_render_conditional_comments($elements) { - $browsers = isset($elements['#browsers']) ? $elements['#browsers'] : array(); - $browsers += array( - 'IE' => TRUE, - '!IE' => TRUE, - ); - - // If rendering in all browsers, no need for conditional comments. - if ($browsers['IE'] === TRUE && $browsers['!IE']) { - return $elements; - } - - // Determine the conditional comment expression for Internet Explorer to - // evaluate. - if ($browsers['IE'] === TRUE) { - $expression = 'IE'; - } - elseif ($browsers['IE'] === FALSE) { - $expression = '!IE'; - } - else { - $expression = $browsers['IE']; - } - - // Wrap the element's potentially existing #prefix and #suffix properties with - // conditional comment markup. The conditional comment expression is evaluated - // by Internet Explorer only. To control the rendering by other browsers, - // either the "downlevel-hidden" or "downlevel-revealed" technique must be - // used. See http://en.wikipedia.org/wiki/Conditional_comment for details. - $elements += array( - '#prefix' => '', - '#suffix' => '', - ); - if (!$browsers['!IE']) { - // "downlevel-hidden". - $elements['#prefix'] = "\n<!--[if $expression]>\n" . $elements['#prefix']; - $elements['#suffix'] .= "<![endif]-->\n"; - } - else { - // "downlevel-revealed". - $elements['#prefix'] = "\n<!--[if $expression]><!-->\n" . $elements['#prefix']; - $elements['#suffix'] .= "<!--<![endif]-->\n"; - } - - return $elements; -} - -/** - * Pre-render callback: Renders a generic HTML tag with attributes into #markup. - * - * Note: It is the caller's responsibility to sanitize any input parameters. - * This callback does not perform sanitization. - * - * @param array $element - * An associative array containing: - * - #tag: The tag name to output. Typical tags added to the HTML HEAD: - * - meta: To provide meta information, such as a page refresh. - * - link: To refer to stylesheets and other contextual information. - * - script: To load JavaScript. - * The value of #tag is not escaped or sanitized, so do not pass in user - * input. - * - #attributes: (optional) An array of HTML attributes to apply to the - * tag. - * - #value: (optional) A string containing tag content, such as inline - * CSS. - * - #value_prefix: (optional) A string to prepend to #value, e.g. a CDATA - * wrapper prefix. - * - #value_suffix: (optional) A string to append to #value, e.g. a CDATA - * wrapper suffix. - */ -function drupal_pre_render_html_tag($element) { - $attributes = isset($element['#attributes']) ? new Attribute($element['#attributes']) : ''; - if (!isset($element['#value'])) { - // This function is intended for internal use, so we assume that no unsafe - // values are passed in #tag. The attributes are already safe because - // Attribute output is already automatically sanitized. - // @todo Escape this properly instead? https://www.drupal.org/node/2296101 - $markup = SafeMarkup::set('<' . $element['#tag'] . $attributes . " />\n"); - } - else { - $markup = '<' . $element['#tag'] . $attributes . '>'; - if (isset($element['#value_prefix'])) { - $markup .= $element['#value_prefix']; - } - $markup .= $element['#value']; - if (isset($element['#value_suffix'])) { - $markup .= $element['#value_suffix']; - } - $markup .= '</' . $element['#tag'] . ">\n"; - // @todo We cannot actually guarantee this markup is safe. Consider a fix - // in: https://www.drupal.org/node/2296101 - $markup = SafeMarkup::set($markup); - } - if (!empty($element['#noscript'])) { - $element['#markup'] = '<noscript>' . $markup . '</noscript>'; - } - else { - $element['#markup'] = $markup; - } - return $element; -} - /** * Pre-render callback: Renders a link into #markup. * @@ -2797,26 +2567,6 @@ function drupal_pre_render_links($element) { return $element; } -/** - * Pre-render callback: Attaches the dropbutton library and required markup. - */ -function drupal_pre_render_dropbutton($element) { - $element['#attached']['library'][] = 'core/drupal.dropbutton'; - $element['#attributes']['class'][] = 'dropbutton'; - if (!isset($element['#theme_wrappers'])) { - $element['#theme_wrappers'] = array(); - } - array_unshift($element['#theme_wrappers'], 'dropbutton_wrapper'); - - // Enable targeted theming of specific dropbuttons (e.g., 'operations' or - // 'operations__node'). - if (isset($element['#subtype'])) { - $element['#theme'] .= '__' . $element['#subtype']; - } - - return $element; -} - /** * Processes the page render array, enhancing it as necessary. * diff --git a/core/includes/form.inc b/core/includes/form.inc index 05c604dc4b066d59efe9d009d1a95cc8794b8fbf..68b5b7195f49a66bacedd8e47a569150acad513e 100644 --- a/core/includes/form.inc +++ b/core/includes/form.inc @@ -5,9 +5,7 @@ * Functions for form and batch generation and processing. */ -use Drupal\Component\Utility\Color; use Drupal\Component\Utility\NestedArray; -use Drupal\Component\Utility\Number; use Drupal\Component\Utility\SafeMarkup; use Drupal\Component\Utility\String; use Drupal\Component\Utility\UrlHelper; @@ -241,332 +239,6 @@ function form_state_values_clean(FormStateInterface $form_state) { } } -/** - * Determines the value for an image button form element. - * - * @param $form - * The form element whose value is being populated. - * @param $input - * The incoming input to populate the form element. If this is FALSE, - * the element's default value should be returned. - * @param $form_state - * The current state of the form. - * - * @return - * The data that will appear in the $form_state->getValues() collection - * for this element. Return nothing to use the default. - */ -function form_type_image_button_value($form, $input, FormStateInterface $form_state) { - if ($input !== FALSE) { - if (!empty($input)) { - // If we're dealing with Mozilla or Opera, we're lucky. It will - // return a proper value, and we can get on with things. - return $form['#return_value']; - } - else { - // Unfortunately, in IE we never get back a proper value for THIS - // form element. Instead, we get back two split values: one for the - // X and one for the Y coordinates on which the user clicked the - // button. We'll find this element in the #post data, and search - // in the same spot for its name, with '_x'. - $input = $form_state->getUserInput(); - foreach (explode('[', $form['#name']) as $element_name) { - // chop off the ] that may exist. - if (substr($element_name, -1) == ']') { - $element_name = substr($element_name, 0, -1); - } - - if (!isset($input[$element_name])) { - if (isset($input[$element_name . '_x'])) { - return $form['#return_value']; - } - return NULL; - } - $input = $input[$element_name]; - } - return $form['#return_value']; - } - } -} - -/** - * Determines the value for a checkbox form element. - * - * @param $form - * The form element whose value is being populated. - * @param $input - * The incoming input to populate the form element. If this is FALSE, - * the element's default value should be returned. - * - * @return - * The data that will appear in the $element_state['values'] collection - * for this element. Return nothing to use the default. - */ -function form_type_checkbox_value($element, $input = FALSE) { - if ($input === FALSE) { - // Use #default_value as the default value of a checkbox, except change - // NULL to 0, because FormBuilder::handleInputElement() would otherwise - // replace NULL with empty string, but an empty string is a potentially - // valid value for a checked checkbox. - return isset($element['#default_value']) ? $element['#default_value'] : 0; - } - else { - // Checked checkboxes are submitted with a value (possibly '0' or ''): - // http://www.w3.org/TR/html401/interact/forms.html#successful-controls. - // For checked checkboxes, browsers submit the string version of - // #return_value, but we return the original #return_value. For unchecked - // checkboxes, browsers submit nothing at all, but - // FormBuilder::handleInputElement() detects this, and calls this - // function with $input=NULL. Returning NULL from a value callback means to - // use the default value, which is not what is wanted when an unchecked - // checkbox is submitted, so we use integer 0 as the value indicating an - // unchecked checkbox. Therefore, modules must not use integer 0 as a - // #return_value, as doing so results in the checkbox always being treated - // as unchecked. The string '0' is allowed for #return_value. The most - // common use-case for setting #return_value to either 0 or '0' is for the - // first option within a 0-indexed array of checkboxes, and for this, - // form_process_checkboxes() uses the string rather than the integer. - return isset($input) ? $element['#return_value'] : 0; - } -} - -/** - * Determines the value for a checkboxes form element. - * - * @param $element - * The form element whose value is being populated. - * @param $input - * The incoming input to populate the form element. If this is FALSE, - * the element's default value should be returned. - * - * @return - * The data that will appear in the $element_state['values'] collection - * for this element. Return nothing to use the default. - */ -function form_type_checkboxes_value($element, $input = FALSE) { - if ($input === FALSE) { - $value = array(); - $element += array('#default_value' => array()); - foreach ($element['#default_value'] as $key) { - $value[$key] = $key; - } - return $value; - } - elseif (is_array($input)) { - // Programmatic form submissions use NULL to indicate that a checkbox - // should be unchecked; see drupal_form_submit(). We therefore remove all - // NULL elements from the array before constructing the return value, to - // simulate the behavior of web browsers (which do not send unchecked - // checkboxes to the server at all). This will not affect non-programmatic - // form submissions, since all values in \Drupal::request()->request are - // strings. - foreach ($input as $key => $value) { - if (!isset($value)) { - unset($input[$key]); - } - } - return array_combine($input, $input); - } - else { - return array(); - } -} - -/** - * Determines the value of a table form element. - * - * @param array $element - * The form element whose value is being populated. - * @param array|false $input - * The incoming input to populate the form element. If this is FALSE, - * the element's default value should be returned. - * - * @return array - * The data that will appear in the $form_state->getValues() collection - * for this element. Return nothing to use the default. - */ -function form_type_table_value(array $element, $input = FALSE) { - // If #multiple is FALSE, the regular default value of radio buttons is used. - if (!empty($element['#tableselect']) && !empty($element['#multiple'])) { - // Contrary to #type 'checkboxes', the default value of checkboxes in a - // table is built from the array keys (instead of array values) of the - // #default_value property. - // @todo D8: Remove this inconsistency. - if ($input === FALSE) { - $element += array('#default_value' => array()); - $value = array_keys(array_filter($element['#default_value'])); - return array_combine($value, $value); - } - else { - return is_array($input) ? array_combine($input, $input) : array(); - } - } -} - -/** - * Form value callback: Determines the value for a #type radios form element. - * - * @param $element - * The form element whose value is being populated. - * @param $input - * (optional) The incoming input to populate the form element. If FALSE, the - * element's default value is returned. Defaults to FALSE. - * - * @return - * The data that will appear in the $element_state['values'] collection for - * this element. - */ -function form_type_radios_value(&$element, $input = FALSE) { - if ($input !== FALSE) { - // When there's user input (including NULL), return it as the value. - // However, if NULL is submitted, FormBuilder::handleInputElement() will - // apply the default value, and we want that validated against #options - // unless it's empty. (An empty #default_value, such as NULL or FALSE, can - // be used to indicate that no radio button is selected by default.) - if (!isset($input) && !empty($element['#default_value'])) { - $element['#needs_validation'] = TRUE; - } - return $input; - } - else { - // For default value handling, simply return #default_value. Additionally, - // for a NULL default value, set #has_garbage_value to prevent - // FormBuilder::handleInputElement() converting the NULL to an empty - // string, so that code can distinguish between nothing selected and the - // selection of a radio button whose value is an empty string. - $value = isset($element['#default_value']) ? $element['#default_value'] : NULL; - if (!isset($value)) { - $element['#has_garbage_value'] = TRUE; - } - return $value; - } -} - -/** - * Determines the value for a tableselect form element. - * - * @param $element - * The form element whose value is being populated. - * @param $input - * The incoming input to populate the form element. If this is FALSE, - * the element's default value should be returned. - * - * @return - * The data that will appear in the $element_state['values'] collection - * for this element. Return nothing to use the default. - */ -function form_type_tableselect_value($element, $input = FALSE) { - // If $element['#multiple'] == FALSE, then radio buttons are displayed and - // the default value handling is used. - if (isset($element['#multiple']) && $element['#multiple']) { - // Checkboxes are being displayed with the default value coming from the - // keys of the #default_value property. This differs from the checkboxes - // element which uses the array values. - if ($input === FALSE) { - $value = array(); - $element += array('#default_value' => array()); - foreach ($element['#default_value'] as $key => $flag) { - if ($flag) { - $value[$key] = $key; - } - } - return $value; - } - else { - return is_array($input) ? array_combine($input, $input) : array(); - } - } -} - -/** - * Determines the value for a password_confirm form element. - * - * @param $element - * The form element whose value is being populated. - * @param $input - * The incoming input to populate the form element. If this is FALSE, - * the element's default value should be returned. - * - * @return - * The data that will appear in the $element_state['values'] collection - * for this element. Return nothing to use the default. - */ -function form_type_password_confirm_value($element, $input = FALSE) { - if ($input === FALSE) { - $element += array('#default_value' => array()); - return $element['#default_value'] + array('pass1' => '', 'pass2' => ''); - } -} - -/** - * Determines the value for a select form element. - * - * @param $element - * The form element whose value is being populated. - * @param $input - * The incoming input to populate the form element. If this is FALSE, - * the element's default value should be returned. - * - * @return - * The data that will appear in the $element_state['values'] collection - * for this element. Return nothing to use the default. - */ -function form_type_select_value($element, $input = FALSE) { - if ($input !== FALSE) { - if (isset($element['#multiple']) && $element['#multiple']) { - // If an enabled multi-select submits NULL, it means all items are - // unselected. A disabled multi-select always submits NULL, and the - // default value should be used. - if (empty($element['#disabled'])) { - return (is_array($input)) ? array_combine($input, $input) : array(); - } - else { - return (isset($element['#default_value']) && is_array($element['#default_value'])) ? $element['#default_value'] : array(); - } - } - // Non-multiple select elements may have an empty option preprended to them - // (see form_process_select()). When this occurs, usually #empty_value is - // an empty string, but some forms set #empty_value to integer 0 or some - // other non-string constant. PHP receives all submitted form input as - // strings, but if the empty option is selected, set the value to match the - // empty value exactly. - elseif (isset($element['#empty_value']) && $input === (string) $element['#empty_value']) { - return $element['#empty_value']; - } - else { - return $input; - } - } -} - -/** - * Determines the value for a textfield form element. - * - * @deprecated Use \Drupal\Core\Render\Element\Textfield::valueCallback(). - */ -function form_type_textfield_value(&$element, $input, &$form_state) { - return Element\Textfield::valueCallback($element, $input, $form_state); -} - -/** - * Determines the value for form's token value. - * - * @param $element - * The form element whose value is being populated. - * @param $input - * The incoming input to populate the form element. If this is FALSE, - * the element's default value should be returned. - * - * @return - * The data that will appear in the $element_state['values'] collection - * for this element. Return nothing to use the default. - */ -function form_type_token_value($element, $input = FALSE) { - if ($input !== FALSE) { - return (string) $input; - } -} - /** * Changes submitted form values during form validation. * @@ -599,74 +271,6 @@ function form_options_flatten($array) { return OptGroup::flattenOptions($array); } -/** - * Processes a select list form element. - * - * This process callback is mandatory for select fields, since all user agents - * automatically preselect the first available option of single (non-multiple) - * select lists. - * - * @param $element - * The form element to process. Properties used: - * - #multiple: (optional) Indicates whether one or more options can be - * selected. Defaults to FALSE. - * - #default_value: Must be NULL or not set in case there is no value for the - * element yet, in which case a first default option is inserted by default. - * Whether this first option is a valid option depends on whether the field - * is #required or not. - * - #required: (optional) Whether the user needs to select an option (TRUE) - * or not (FALSE). Defaults to FALSE. - * - #empty_option: (optional) The label to show for the first default option. - * By default, the label is automatically set to "- Please select -" for a - * required field and "- None -" for an optional field. - * - #empty_value: (optional) The value for the first default option, which is - * used to determine whether the user submitted a value or not. - * - If #required is TRUE, this defaults to '' (an empty string). - * - If #required is not TRUE and this value isn't set, then no extra option - * is added to the select control, leaving the control in a slightly - * illogical state, because there's no way for the user to select nothing, - * since all user agents automatically preselect the first available - * option. But people are used to this being the behavior of select - * controls. - * @todo Address the above issue in Drupal 8. - * - If #required is not TRUE and this value is set (most commonly to an - * empty string), then an extra option (see #empty_option above) - * representing a "non-selection" is added with this as its value. - * - * @see _form_validate() - */ -function form_process_select($element) { - // #multiple select fields need a special #name. - if ($element['#multiple']) { - $element['#attributes']['multiple'] = 'multiple'; - $element['#attributes']['name'] = $element['#name'] . '[]'; - } - // A non-#multiple select needs special handling to prevent user agents from - // preselecting the first option without intention. #multiple select lists do - // not get an empty option, as it would not make sense, user interface-wise. - else { - // If the element is set to #required through #states, override the - // element's #required setting. - $required = isset($element['#states']['required']) ? TRUE : $element['#required']; - // If the element is required and there is no #default_value, then add an - // empty option that will fail validation, so that the user is required to - // make a choice. Also, if there's a value for #empty_value or - // #empty_option, then add an option that represents emptiness. - if (($required && !isset($element['#default_value'])) || isset($element['#empty_value']) || isset($element['#empty_option'])) { - $element += array( - '#empty_value' => '', - '#empty_option' => $required ? t('- Select -') : t('- None -'), - ); - // The empty option is prepended to #options and purposively not merged - // to prevent another option in #options mistakenly using the same value - // as #empty_value. - $empty_option = array($element['#empty_value'] => $element['#empty_option']); - $element['#options'] = $empty_option + $element['#options']; - } - } - return $element; -} - /** * Prepares variables for select element templates. * @@ -685,7 +289,7 @@ function form_process_select($element) { function template_preprocess_select(&$variables) { $element = $variables['element']; Element::setAttributes($element, array('id', 'name', 'size')); - _form_set_attributes($element, array('form-select')); + Element\RenderElement::setAttributes($element, array('form-select')); $variables['attributes'] = $element['#attributes']; $variables['options'] = form_select_options($element); @@ -806,7 +410,7 @@ function form_get_options($element, $key) { function template_preprocess_fieldset(&$variables) { $element = $variables['element']; Element::setAttributes($element, array('id')); - _form_set_attributes($element, array('form-wrapper')); + Element\RenderElement::setAttributes($element, array('form-wrapper')); $variables['attributes'] = $element['#attributes']; $variables['attributes']['class'][] = 'form-item'; $variables['prefix'] = isset($element['#field_prefix']) ? $element['#field_prefix'] : NULL; @@ -866,33 +470,6 @@ function template_preprocess_details(&$variables) { $variables['value'] = (isset($element['#value'])) ? $element['#value'] : ''; } -/** - * Prepares a #type 'radio' render element for theme_input(). - * - * @param array $element - * An associative array containing the properties of the element. - * Properties used: #required, #return_value, #value, #attributes, #title, - * #description. - * - * Note: The input "name" attribute needs to be sanitized before output, which - * is currently done by initializing Drupal\Core\Template\Attribute with - * all the attributes. - * - * @return array - * The $element with prepared variables ready for theme_input(). - */ -function form_pre_render_radio($element) { - $element['#attributes']['type'] = 'radio'; - Element::setAttributes($element, array('id', 'name', '#return_value' => 'value')); - - if (isset($element['#return_value']) && $element['#value'] !== FALSE && $element['#value'] == $element['#return_value']) { - $element['#attributes']['checked'] = 'checked'; - } - _form_set_attributes($element, array('form-radio')); - - return $element; -} - /** * Prepares variables for radios templates. * @@ -920,160 +497,6 @@ function template_preprocess_radios(&$variables) { $variables['children'] = $element['#children']; } -/** - * Expand a password_confirm field into two text boxes. - */ -function form_process_password_confirm($element) { - $element['pass1'] = array( - '#type' => 'password', - '#title' => t('Password'), - '#value' => empty($element['#value']) ? NULL : $element['#value']['pass1'], - '#required' => $element['#required'], - '#attributes' => array('class' => array('password-field')), - ); - $element['pass2'] = array( - '#type' => 'password', - '#title' => t('Confirm password'), - '#value' => empty($element['#value']) ? NULL : $element['#value']['pass2'], - '#required' => $element['#required'], - '#attributes' => array('class' => array('password-confirm')), - ); - $element['#element_validate'] = array('password_confirm_validate'); - $element['#tree'] = TRUE; - - if (isset($element['#size'])) { - $element['pass1']['#size'] = $element['pass2']['#size'] = $element['#size']; - } - - return $element; -} - -/** - * Validates a password_confirm element. - */ -function password_confirm_validate($element, &$element_state) { - $pass1 = trim($element['pass1']['#value']); - $pass2 = trim($element['pass2']['#value']); - if (!empty($pass1) || !empty($pass2)) { - if (strcmp($pass1, $pass2)) { - form_error($element, $element_state, t('The specified passwords do not match.')); - } - } - elseif ($element['#required'] && !empty($element_state['input'])) { - form_error($element, $element_state, t('Password field is required.')); - } - - // Password field must be converted from a two-element array into a single - // string regardless of validation results. - form_set_value($element['pass1'], NULL, $element_state); - form_set_value($element['pass2'], NULL, $element_state); - form_set_value($element, $pass1, $element_state); - - return $element; - -} - -/** - * Adds form-specific attributes to a 'date' #type element. - * - * Supports HTML5 types of 'date', 'datetime', 'datetime-local', and 'time'. - * Falls back to a plain textfield. Used as a sub-element by the datetime - * element type. - * - * @param array $element - * An associative array containing the properties of the element. - * Properties used: #title, #value, #options, #description, #required, - * #attributes, #id, #name, #type, #min, #max, #step, #value, #size. - * - * Note: The input "name" attribute needs to be sanitized before output, which - * is currently done by initializing Drupal\Core\Template\Attribute with - * all the attributes. - * - * @return array - * The $element with prepared variables ready for #theme 'input__date'. - */ -function form_pre_render_date($element) { - if (empty($element['#attributes']['type'])) { - $element['#attributes']['type'] = 'date'; - } - Element::setAttributes($element, array('id', 'name', 'type', 'min', 'max', 'step', 'value', 'size')); - _form_set_attributes($element, array('form-' . $element['#attributes']['type'])); - - return $element; -} - -/** - * Sets the value for a weight element, with zero as a default. - */ -function weight_value(&$form) { - if (isset($form['#default_value'])) { - $form['#value'] = $form['#default_value']; - } - else { - $form['#value'] = 0; - } -} - -/** - * Expands a radios element into individual radio elements. - */ -function form_process_radios($element) { - if (count($element['#options']) > 0) { - $weight = 0; - foreach ($element['#options'] as $key => $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()); - // Generate the parents as the autogenerator does, so we will have a - // unique id for each radio button. - $parents_for_id = array_merge($element['#parents'], array($key)); - $element[$key] += array( - '#type' => 'radio', - '#title' => $choice, - // The key is sanitized in Drupal\Core\Template\Attribute during output - // from the theme function. - '#return_value' => $key, - // Use default or FALSE. A value of FALSE means that the radio button is - // not 'checked'. - '#default_value' => isset($element['#default_value']) ? $element['#default_value'] : FALSE, - '#attributes' => $element['#attributes'], - '#parents' => $element['#parents'], - '#id' => drupal_html_id('edit-' . implode('-', $parents_for_id)), - '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL, - '#weight' => $weight, - ); - } - } - return $element; -} - -/** - * Prepares a #type 'checkbox' render element for theme_input(). - * - * @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 theme_input(). - */ -function form_pre_render_checkbox($element) { - $element['#attributes']['type'] = 'checkbox'; - Element::setAttributes($element, array('id', 'name', '#return_value' => 'value')); - - // Unchecked checkbox has #value of integer 0. - if (!empty($element['#checked'])) { - $element['#attributes']['checked'] = 'checked'; - } - _form_set_attributes($element, array('form-checkbox')); - - return $element; -} - /** * Prepares variables for checkboxes templates. * @@ -1102,1254 +525,61 @@ function template_preprocess_checkboxes(&$variables) { } /** - * Adds form element theming to an element if its title or description is set. + * Prepares variables for vertical tabs templates. + * + * Default template: vertical-tabs.html.twig. + * + * @param array $variables + * An associative array containing: + * - element: An associative array containing the properties and children of + * the details element. Properties used: #children. * - * This is used as a pre render function for checkboxes and radios. - */ -function form_pre_render_conditional_form_element($element) { - // Set the element's title attribute to show #title as a tooltip, if needed. - if (isset($element['#title']) && $element['#title_display'] == 'attribute') { - $element['#attributes']['title'] = $element['#title']; - if (!empty($element['#required'])) { - // Append an indication that this field is required. - $element['#attributes']['title'] .= ' (' . t('Required') . ')'; - } - } - - if (isset($element['#title']) || isset($element['#description'])) { - // @see #type 'fieldgroup' - $element['#theme_wrappers'][] = 'fieldset'; - $element['#attributes']['class'][] = 'fieldgroup'; - $element['#attributes']['class'][] = 'form-composite'; - } - return $element; -} - -/** - * Processes a form button element. - */ -function form_process_button($element, FormStateInterface $form_state) { - // If this is a button intentionally allowing incomplete form submission - // (e.g., a "Previous" or "Add another item" button), then also skip - // client-side validation. - if (isset($element['#limit_validation_errors']) && $element['#limit_validation_errors'] !== FALSE) { - $element['#attributes']['formnovalidate'] = 'formnovalidate'; - } - return $element; -} - -/** - * Sets the #checked property of a checkbox element. - */ -function form_process_checkbox($element, FormStateInterface $form_state) { - $value = $element['#value']; - $return_value = $element['#return_value']; - // On form submission, the #value of an available and enabled checked - // checkbox is #return_value, and the #value of an available and enabled - // unchecked checkbox is integer 0. On not submitted forms, and for - // checkboxes with #access=FALSE or #disabled=TRUE, the #value is - // #default_value (integer 0 if #default_value is NULL). Most of the time, - // a string comparison of #value and #return_value is sufficient for - // determining the "checked" state, but a value of TRUE always means checked - // (even if #return_value is 'foo'), and a value of FALSE or integer 0 always - // means unchecked (even if #return_value is '' or '0'). - if ($value === TRUE || $value === FALSE || $value === 0) { - $element['#checked'] = (bool) $value; - } - else { - // Compare as strings, so that 15 is not considered equal to '15foo', but 1 - // is considered equal to '1'. This cast does not imply that either #value - // or #return_value is expected to be a string. - $element['#checked'] = ((string) $value === (string) $return_value); - } - return $element; -} - -/** - * Processes a checkboxes form element. */ -function form_process_checkboxes($element) { - $value = is_array($element['#value']) ? $element['#value'] : array(); - $element['#tree'] = TRUE; - if (count($element['#options']) > 0) { - if (!isset($element['#default_value']) || $element['#default_value'] == 0) { - $element['#default_value'] = array(); - } - $weight = 0; - foreach ($element['#options'] as $key => $choice) { - // Integer 0 is not a valid #return_value, so use '0' instead. - // @see form_type_checkbox_value(). - // @todo For Drupal 8, cast all integer keys to strings for consistency - // with form_process_radios(). - if ($key === 0) { - $key = '0'; - } - // 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' => 'checkbox', - '#title' => $choice, - '#return_value' => $key, - '#default_value' => isset($value[$key]) ? $key : NULL, - '#attributes' => $element['#attributes'], - '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL, - '#weight' => $weight, - ); - } - } - return $element; +function template_preprocess_vertical_tabs(&$variables) { + $element = $variables['element']; + $variables['children'] = (!empty($element['#children'])) ? $element['#children'] : ''; } /** - * Processes a form actions container element. + * Prepares variables for input templates. * - * @param $element - * An associative array containing the properties and children of the - * form actions container. - * @param $form_state - * The current state of the form for the form this element belongs to. + * Default template: input.html.twig. * - * @return - * The processed element. + * @param array $variables + * An associative array containing: + * - element: An associative array containing the properties of the element. + * Properties used: #attributes. */ -function form_process_actions($element, FormStateInterface $form_state) { - $element['#attributes']['class'][] = 'form-actions'; - return $element; +function template_preprocess_input(&$variables) { + $element = $variables['element']; + $variables['children'] = $element['#children']; } /** - * #pre_render callback for #type 'actions'. - * - * This callback iterates over all child elements of the #type 'actions' - * container to look for elements with a #dropbutton property, so as to group - * those elements into dropbuttons. As such, it works similar to #group, but is - * specialized for dropbuttons. - * - * The value of #dropbutton denotes the dropbutton to group the child element - * into. For example, two different values of 'foo' and 'bar' on child elements - * would generate two separate dropbuttons, which each contain the corresponding - * buttons. + * Prepares variables for form templates. * - * @param array $element - * The #type 'actions' element to process. + * Default template: form.html.twig. * - * @return array - * The processed #type 'actions' element, including individual buttons grouped - * into new #type 'dropbutton' elements. + * @param $variables + * An associative array containing: + * - element: An associative array containing the properties of the element. + * Properties used: #action, #method, #attributes, #children */ -function form_pre_render_actions_dropbutton(array $element) { - $dropbuttons = array(); - foreach (Element::children($element, TRUE) as $key) { - if (isset($element[$key]['#dropbutton'])) { - $dropbutton = $element[$key]['#dropbutton']; - // If there is no dropbutton for this button group yet, create one. - if (!isset($dropbuttons[$dropbutton])) { - $dropbuttons[$dropbutton] = array( - '#type' => 'dropbutton', - ); - } - // Add this button to the corresponding dropbutton. - // @todo Change #type 'dropbutton' to be based on theme_item_list() - // instead of links.html.twig to avoid this preemptive rendering. - $button = drupal_render($element[$key]); - $dropbuttons[$dropbutton]['#links'][$key] = array( - 'title' => $button, - 'html' => TRUE, - ); - } +function template_preprocess_form(&$variables) { + $element = $variables['element']; + if (isset($element['#action'])) { + $element['#attributes']['action'] = UrlHelper::stripDangerousProtocols($element['#action']); } - // @todo For now, all dropbuttons appear first. Consider to invent a more - // fancy sorting/injection algorithm here. - return $dropbuttons + $element; + Element::setAttributes($element, array('method', 'id')); + if (empty($element['#attributes']['accept-charset'])) { + $element['#attributes']['accept-charset'] = "UTF-8"; + } + $variables['attributes'] = $element['#attributes']; + $variables['children'] = $element['#children']; } /** - * #process callback for #pattern form element property. - * - * @deprecated Use \Drupal\Core\Render\Element\FormElement::processPattern(). - */ -function form_process_pattern($element, FormStateInterface $form_state, &$complete_form) { - return Element\FormElement::processPattern($element, $form_state, $complete_form); -} - -/** - * #element_validate callback for #pattern form element property. - * - * @param $element - * An associative array containing the properties and children of the - * generic form element. - * @param $form_state - * The current state of the form for the form this element belongs to. - * - * @see form_process_pattern() - */ -function form_validate_pattern($element, FormStateInterface $form_state) { - if ($element['#value'] !== '') { - // The pattern must match the entire string and should have the same - // behavior as the RegExp object in ECMA 262. - // - Use bracket-style delimiters to avoid introducing a special delimiter - // character like '/' that would have to be escaped. - // - Put in brackets so that the pattern can't interfere with what's - // prepended and appended. - $pattern = '{^(?:' . $element['#pattern'] . ')$}'; - - if (!preg_match($pattern, $element['#value'])) { - form_error($element, $form_state, t('%name field is not in the right format.', array('%name' => $element['#title']))); - } - } -} - -/** - * Processes a container element. - * - * @param $element - * An associative array containing the properties and children of the - * container. - * @param $form_state - * The current state of the form for the form this element belongs to. - * - * @return - * The processed element. - */ -function form_process_container($element, FormStateInterface $form_state) { - // Generate the ID of the element if it's not explicitly given. - if (!isset($element['#id'])) { - $element['#id'] = drupal_html_id(implode('-', $element['#parents']) . '-wrapper'); - } - return $element; -} - -/** - * Prepares a 'tableselect' #type element for rendering. - * - * Adds a column of radio buttons or checkboxes for each row of a table. - * - * @param array $element - * An associative array containing the properties and children of - * the tableselect element. Properties used: #header, #options, #empty, - * and #js_select. The #options property is an array of selection options; - * each array element of #options is an array of properties. These - * properties can include #attributes, which is added to the - * table row's HTML attributes; see table.html.twig. An example of per-row - * options: - * @code - * $options = array( - * array( - * 'title' => 'How to Learn Drupal', - * 'content_type' => 'Article', - * 'status' => 'published', - * '#attributes' => array('class' => array('article-row')), - * ), - * array( - * 'title' => 'Privacy Policy', - * 'content_type' => 'Page', - * 'status' => 'published', - * '#attributes' => array('class' => array('page-row')), - * ), - * ); - * $header = array( - * 'title' => t('Title'), - * 'content_type' => t('Content type'), - * 'status' => t('Status'), - * ); - * $form['table'] = array( - * '#type' => 'tableselect', - * '#header' => $header, - * '#options' => $options, - * '#empty' => t('No content available.'), - * ); - * @endcode - * - * @return array - * The processed element. - */ -function form_pre_render_tableselect($element) { - $rows = array(); - $header = $element['#header']; - if (!empty($element['#options'])) { - // Generate a table row for each selectable item in #options. - foreach (Element::children($element) as $key) { - $row = array(); - - $row['data'] = array(); - if (isset($element['#options'][$key]['#attributes'])) { - $row += $element['#options'][$key]['#attributes']; - } - // Render the checkbox / radio element. - $row['data'][] = drupal_render($element[$key]); - - // As theme_table only maps header and row columns by order, create the - // correct order by iterating over the header fields. - foreach ($element['#header'] as $fieldname => $title) { - // A row cell can span over multiple headers, which means less row cells - // than headers could be present. - if (isset($element['#options'][$key][$fieldname])) { - // A header can span over multiple cells and in this case the cells - // are passed in an array. The order of this array determines the - // order in which they are added. - if (is_array($element['#options'][$key][$fieldname]) && !isset($element['#options'][$key][$fieldname]['data'])) { - foreach ($element['#options'][$key][$fieldname] as $cell) { - $row['data'][] = $cell; - } - } - else { - $row['data'][] = $element['#options'][$key][$fieldname]; - } - } - } - $rows[] = $row; - } - // Add an empty header or a "Select all" checkbox to provide room for the - // checkboxes/radios in the first table column. - if ($element['#js_select']) { - // Add a "Select all" checkbox. - $element['#attached']['library'][] = 'core/drupal.tableselect'; - array_unshift($header, array('class' => array('select-all'))); - } - else { - // Add an empty header when radio buttons are displayed or a "Select all" - // checkbox is not desired. - array_unshift($header, ''); - } - } - - $element['#header'] = $header; - $element['#rows'] = $rows; - - return $element; -} - -/** - * Creates checkbox or radio elements to populate a tableselect table. - * - * @param $element - * An associative array containing the properties and children of the - * tableselect element. - * - * @return - * The processed element. - */ -function form_process_tableselect($element) { - - if ($element['#multiple']) { - $value = is_array($element['#value']) ? $element['#value'] : array(); - } - else { - // Advanced selection behavior makes no sense for radios. - $element['#js_select'] = FALSE; - } - - $element['#tree'] = TRUE; - - if (count($element['#options']) > 0) { - if (!isset($element['#default_value']) || $element['#default_value'] === 0) { - $element['#default_value'] = array(); - } - - // Create a checkbox or radio for each item in #options in such a way that - // the value of the tableselect element behaves as if it had been of type - // checkboxes or radios. - foreach ($element['#options'] as $key => $choice) { - // Do not overwrite manually created children. - if (!isset($element[$key])) { - if ($element['#multiple']) { - $title = ''; - if (!empty($element['#options'][$key]['title']['data']['#title'])) { - $title = t('Update @title', array( - '@title' => $element['#options'][$key]['title']['data']['#title'], - )); - } - $element[$key] = array( - '#type' => 'checkbox', - '#title' => $title, - '#title_display' => 'invisible', - '#return_value' => $key, - '#default_value' => isset($value[$key]) ? $key : NULL, - '#attributes' => $element['#attributes'], - ); - } - else { - // Generate the parents as the autogenerator does, so we will have a - // unique id for each radio button. - $parents_for_id = array_merge($element['#parents'], array($key)); - $element[$key] = array( - '#type' => 'radio', - '#title' => '', - '#return_value' => $key, - '#default_value' => ($element['#default_value'] == $key) ? $key : NULL, - '#attributes' => $element['#attributes'], - '#parents' => $element['#parents'], - '#id' => drupal_html_id('edit-' . implode('-', $parents_for_id)), - '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL, - ); - } - if (isset($element['#options'][$key]['#weight'])) { - $element[$key]['#weight'] = $element['#options'][$key]['#weight']; - } - } - } - } - else { - $element['#value'] = array(); - } - return $element; -} - -/** - * #process callback for #type 'table' to add tableselect support. - * - * @param array $element - * An associative array containing the properties and children of the - * table element. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - * - * @return array - * The processed element. - * - * @see form_process_tableselect() - * @see form_pre_render_tableselect() - */ -function form_process_table($element, FormStateInterface $form_state) { - if ($element['#tableselect']) { - if ($element['#multiple']) { - $value = is_array($element['#value']) ? $element['#value'] : array(); - } - // Advanced selection behavior makes no sense for radios. - else { - $element['#js_select'] = FALSE; - } - // Add a "Select all" checkbox column to the header. - // @todo D8: Rename into #select_all? - if ($element['#js_select']) { - $element['#attached']['library'][] = 'core/drupal.tableselect'; - array_unshift($element['#header'], array('class' => array('select-all'))); - } - // Add an empty header column for radio buttons or when a "Select all" - // checkbox is not desired. - else { - array_unshift($element['#header'], ''); - } - - if (!isset($element['#default_value']) || $element['#default_value'] === 0) { - $element['#default_value'] = array(); - } - // Create a checkbox or radio for each row in a way that the value of the - // tableselect element behaves as if it had been of #type checkboxes or - // radios. - foreach (Element::children($element) as $key) { - $row = &$element[$key]; - // Prepare the element #parents for the tableselect form element. - // Their values have to be located in child keys (#tree is ignored), since - // form_validate_table() has to be able to validate whether input (for the - // parent #type 'table' element) has been submitted. - $element_parents = array_merge($element['#parents'], array($key)); - - // Since the #parents of the tableselect form element will equal the - // #parents of the row element, prevent FormBuilder from auto-generating - // an #id for the row element, since drupal_html_id() would automatically - // append a suffix to the tableselect form element's #id otherwise. - $row['#id'] = drupal_html_id('edit-' . implode('-', $element_parents) . '-row'); - - // Do not overwrite manually created children. - if (!isset($row['select'])) { - // Determine option label; either an assumed 'title' column, or the - // first available column containing a #title or #markup. - // @todo Consider to add an optional $element[$key]['#title_key'] - // defaulting to 'title'? - unset($label_element); - $title = NULL; - if (isset($row['title']['#type']) && $row['title']['#type'] == 'label') { - $label_element = &$row['title']; - } - else { - if (!empty($row['title']['#title'])) { - $title = $row['title']['#title']; - } - else { - foreach (Element::children($row) as $column) { - if (isset($row[$column]['#title'])) { - $title = $row[$column]['#title']; - break; - } - if (isset($row[$column]['#markup'])) { - $title = $row[$column]['#markup']; - break; - } - } - } - if (isset($title) && $title !== '') { - $title = t('Update !title', array('!title' => $title)); - } - } - - // Prepend the select column to existing columns. - $row = array('select' => array()) + $row; - $row['select'] += array( - '#type' => $element['#multiple'] ? 'checkbox' : 'radio', - '#id' => drupal_html_id('edit-' . implode('-', $element_parents)), - // @todo If rows happen to use numeric indexes instead of string keys, - // this results in a first row with $key === 0, which is always FALSE. - '#return_value' => $key, - '#attributes' => $element['#attributes'], - '#wrapper_attributes' => array( - 'class' => array('table-select'), - ), - ); - if ($element['#multiple']) { - $row['select']['#default_value'] = isset($value[$key]) ? $key : NULL; - $row['select']['#parents'] = $element_parents; - } - else { - $row['select']['#default_value'] = ($element['#default_value'] == $key ? $key : NULL); - $row['select']['#parents'] = $element['#parents']; - } - if (isset($label_element)) { - $label_element['#id'] = $row['select']['#id'] . '--label'; - $label_element['#for'] = $row['select']['#id']; - $row['select']['#attributes']['aria-labelledby'] = $label_element['#id']; - $row['select']['#title_display'] = 'none'; - } - else { - $row['select']['#title'] = $title; - $row['select']['#title_display'] = 'invisible'; - } - } - } - } - - return $element; -} - -/** - * #element_validate callback for #type 'table'. - * - * @param array $element - * An associative array containing the properties and children of the - * table element. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - */ -function form_validate_table($element, FormStateInterface $form_state) { - // Skip this validation if the button to submit the form does not require - // selected table row data. - if (empty($form_state['triggering_element']['#tableselect'])) { - return; - } - if ($element['#multiple']) { - if (!is_array($element['#value']) || !count(array_filter($element['#value']))) { - form_error($element, $form_state, t('No items selected.')); - } - } - elseif (!isset($element['#value']) || $element['#value'] === '') { - form_error($element, $form_state, t('No item selected.')); - } -} - -/** - * Processes a machine-readable name form element. - * - * @param $element - * The form element to process. Properties used: - * - #machine_name: An associative array containing: - * - exists: A callable to invoke for checking whether a submitted machine - * name value already exists. The submitted value is passed as an - * argument. In most cases, an existing API or menu argument loader - * function can be re-used. The callback is only invoked, if the submitted - * value differs from the element's #default_value. - * - source: (optional) The #array_parents of the form element containing - * the human-readable name (i.e., as contained in the $form structure) to - * use as source for the machine name. Defaults to array('label'). - * - label: (optional) A text to display as label for the machine name value - * after the human-readable name form element. Defaults to "Machine name". - * - replace_pattern: (optional) A regular expression (without delimiters) - * matching disallowed characters in the machine name. Defaults to - * '[^a-z0-9_]+'. - * - replace: (optional) A character to replace disallowed characters in the - * machine name via JavaScript. Defaults to '_' (underscore). When using a - * different character, 'replace_pattern' needs to be set accordingly. - * - error: (optional) A custom form error message string to show, if the - * machine name contains disallowed characters. - * - standalone: (optional) Whether the live preview should stay in its own - * form element rather than in the suffix of the source element. Defaults - * to FALSE. - * - #maxlength: (optional) Should be set to the maximum allowed length of the - * machine name. Defaults to 64. - * - #disabled: (optional) Should be set to TRUE in case an existing machine - * name must not be changed after initial creation. - */ -function form_process_machine_name($element, FormStateInterface $form_state) { - // We need to pass the langcode to the client. - $language = \Drupal::languageManager()->getCurrentLanguage(); - - // Apply default form element properties. - $element += array( - '#title' => t('Machine-readable name'), - '#description' => t('A unique machine-readable name. Can only contain lowercase letters, numbers, and underscores.'), - '#machine_name' => array(), - '#field_prefix' => '', - '#field_suffix' => '', - '#suffix' => '', - ); - // A form element that only wants to set one #machine_name property (usually - // 'source' only) would leave all other properties undefined, if the defaults - // were defined in hook_element_info(). Therefore, we apply the defaults here. - $element['#machine_name'] += array( - 'source' => array('label'), - 'target' => '#' . $element['#id'], - 'label' => t('Machine name'), - 'replace_pattern' => '[^a-z0-9_]+', - 'replace' => '_', - 'standalone' => FALSE, - 'field_prefix' => $element['#field_prefix'], - 'field_suffix' => $element['#field_suffix'], - ); - - // By default, machine names are restricted to Latin alphanumeric characters. - // So, default to LTR directionality. - if (!isset($element['#attributes'])) { - $element['#attributes'] = array(); - } - $element['#attributes'] += array('dir' => 'ltr'); - - // The source element defaults to array('name'), but may have been overidden. - if (empty($element['#machine_name']['source'])) { - return $element; - } - - // Retrieve the form element containing the human-readable name from the - // complete form in $form_state. By reference, because we may need to append - // a #field_suffix that will hold the live preview. - $key_exists = NULL; - $source = NestedArray::getValue($form_state->getCompleteForm(), $element['#machine_name']['source'], $key_exists); - if (!$key_exists) { - return $element; - } - - $suffix_id = $source['#id'] . '-machine-name-suffix'; - $element['#machine_name']['suffix'] = '#' . $suffix_id; - - if ($element['#machine_name']['standalone']) { - $element['#suffix'] = SafeMarkup::set($element['#suffix'] . ' <small id="' . $suffix_id . '"> </small>'); - } - else { - // Append a field suffix to the source form element, which will contain - // the live preview of the machine name. - $source += array('#field_suffix' => ''); - $source['#field_suffix'] = SafeMarkup::set($source['#field_suffix'] . ' <small id="' . $suffix_id . '"> </small>'); - - $parents = array_merge($element['#machine_name']['source'], array('#field_suffix')); - NestedArray::setValue($form_state->getCompleteForm(), $parents, $source['#field_suffix']); - } - - $js_settings = array( - 'type' => 'setting', - 'data' => array( - 'machineName' => array( - '#' . $source['#id'] => $element['#machine_name'], - ), - 'langcode' => $language->id, - ), - ); - $element['#attached']['library'][] = 'core/drupal.machine-name'; - $element['#attached']['js'][] = $js_settings; - - return $element; -} - -/** - * Form element validation handler for machine_name elements. - * - * Note that #maxlength is validated by _form_validate() already. - */ -function form_validate_machine_name(&$element, FormStateInterface $form_state) { - // Verify that the machine name not only consists of replacement tokens. - if (preg_match('@^' . $element['#machine_name']['replace'] . '+$@', $element['#value'])) { - form_error($element, $form_state, t('The machine-readable name must contain unique characters.')); - } - - // Verify that the machine name contains no disallowed characters. - if (preg_match('@' . $element['#machine_name']['replace_pattern'] . '@', $element['#value'])) { - if (!isset($element['#machine_name']['error'])) { - // Since a hyphen is the most common alternative replacement character, - // a corresponding validation error message is supported here. - if ($element['#machine_name']['replace'] == '-') { - form_error($element, $form_state, t('The machine-readable name must contain only lowercase letters, numbers, and hyphens.')); - } - // Otherwise, we assume the default (underscore). - else { - form_error($element, $form_state, t('The machine-readable name must contain only lowercase letters, numbers, and underscores.')); - } - } - else { - form_error($element, $form_state, $element['#machine_name']['error']); - } - } - - // Verify that the machine name is unique. - if ($element['#default_value'] !== $element['#value']) { - $function = $element['#machine_name']['exists']; - if (call_user_func($function, $element['#value'], $element, $form_state)) { - form_error($element, $form_state, t('The machine-readable name is already in use. It must be unique.')); - } - } -} - -/** - * Arranges elements into groups. - * - * @param $element - * An associative array containing the properties and children of the - * element. Note that $element must be taken by reference here, so processed - * child elements are taken over into $form_state. - * @param $form_state - * The current state of the form for the form this element belongs to. - * - * @return - * The processed element. - */ -function form_process_group(&$element, FormStateInterface $form_state) { - $parents = implode('][', $element['#parents']); - - // Each details element forms a new group. The #type 'vertical_tabs' basically - // only injects a new details element. - $form_state['groups'][$parents]['#group_exists'] = TRUE; - $element['#groups'] = &$form_state['groups']; - - // Process vertical tabs group member details elements. - if (isset($element['#group'])) { - // Add this details element to the defined group (by reference). - $group = $element['#group']; - $form_state['groups'][$group][] = &$element; - } - - return $element; -} - -/** - * Adds form element theming to details. - * - * @param $element - * An associative array containing the properties and children of the - * details. - * - * @return - * The modified element. - */ -function form_pre_render_details($element) { - Element::setAttributes($element, array('id')); - - // The .form-wrapper class is required for #states to treat details like - // containers. - _form_set_attributes($element, array('form-wrapper')); - - // Collapsible details. - $element['#attached']['library'][] = 'core/drupal.collapse'; - if (!empty($element['#open'])) { - $element['#attributes']['open'] = 'open'; - } - - // Do not render optional details elements if there are no children. - if (isset($element['#parents'])) { - $group = implode('][', $element['#parents']); - if (!empty($element['#optional']) && !Element::getVisibleChildren($element['#groups'][$group])) { - $element['#printed'] = TRUE; - } - } - - return $element; -} - -/** - * Adds members of this group as actual elements for rendering. - * - * @deprecated Use \Drupal\Core\Render\ElementElementBase::preRenderGroup(). - */ -function form_pre_render_group($element) { - return Element\RenderElement::preRenderGroup($element); -} - -/** - * Creates a group formatted as vertical tabs. - * - * @param $element - * An associative array containing the properties and children of the - * details element. - * @param $form_state - * The current state of the form for the form this vertical tab widget belongs to. - * - * @return - * The processed element. - */ -function form_process_vertical_tabs($element, FormStateInterface $form_state) { - // Inject a new details as child, so that form_process_details() processes - // this details element like any other details. - $element['group'] = array( - '#type' => 'details', - '#theme_wrappers' => array(), - '#parents' => $element['#parents'], - ); - - // Add an invisible label for accessibility. - if (!isset($element['#title'])) { - $element['#title'] = t('Vertical Tabs'); - $element['#title_display'] = 'invisible'; - } - - $element['#attached']['library'][] = 'core/drupal.vertical-tabs'; - - // The JavaScript stores the currently selected tab in this hidden - // field so that the active tab can be restored the next time the - // form is rendered, e.g. on preview pages or when form validation - // fails. - $name = implode('__', $element['#parents']); - if ($form_state->hasValue($name . '__active_tab')){ - $element['#default_tab'] = $form_state->getValue($name . '__active_tab'); - } - $element[$name . '__active_tab'] = array( - '#type' => 'hidden', - '#default_value' => $element['#default_tab'], - '#attributes' => array('class' => array('vertical-tabs-active-tab')), - ); - - return $element; -} - -/** - * Prepares a vertical_tabs element for rendering. - * - * @param array $element - * An associative array containing the properties and children of the - * vertical tabs element. - * - * @return array - * The modified element. - */ -function form_pre_render_vertical_tabs($element) { - // Do not render the vertical tabs element if it is empty. - $group = implode('][', $element['#parents']); - if (!Element::getVisibleChildren($element['group']['#groups'][$group])) { - $element['#printed'] = TRUE; - } - return $element; -} - -/** - * Prepares variables for vertical tabs templates. - * - * Default template: vertical-tabs.html.twig. - * - * @param array $variables - * An associative array containing: - * - element: An associative array containing the properties and children of - * the details element. Properties used: #children. - * - */ -function template_preprocess_vertical_tabs(&$variables) { - $element = $variables['element']; - $variables['children'] = (!empty($element['#children'])) ? $element['#children'] : ''; -} - -/** - * Adds autocomplete functionality to elements with a valid - * #autocomplete_route_name. - * - * @deprecated Use \Drupal\Core\Render\Element\FormElement::processAutocomplete(). - */ -function form_process_autocomplete($element, FormStateInterface $form_state, &$complete_form) { - return Element\FormElement::processAutocomplete($element, $form_state, $complete_form); -} - -/** - * Prepares variables for input templates. - * - * Default template: input.html.twig. - * - * @param array $variables - * An associative array containing: - * - element: An associative array containing the properties of the element. - * Properties used: #attributes. - */ -function template_preprocess_input(&$variables) { - $element = $variables['element']; - $variables['children'] = $element['#children']; -} - -/** - * Prepares a #type 'button' render element for theme_input(). - * - * @param array $element - * An associative array containing the properties of the element. - * Properties used: #attributes, #button_type, #name, #value. - * - * The #button_type property accepts any value, though core themes have CSS that - * styles the following button_types appropriately: 'primary', 'danger'. - * - * @return array - * The $element with prepared variables ready for theme_input(). - */ -function form_pre_render_button($element) { - $element['#attributes']['type'] = 'submit'; - Element::setAttributes($element, array('id', 'name', 'value')); - - $element['#attributes']['class'][] = 'button'; - if (!empty($element['#button_type'])) { - $element['#attributes']['class'][] = 'button--' . $element['#button_type']; - } - // @todo Various JavaScript depends on this button class. - $element['#attributes']['class'][] = 'form-submit'; - - if (!empty($element['#attributes']['disabled'])) { - $element['#attributes']['class'][] = 'is-disabled'; - } - - return $element; -} - -/** - * Prepares a #type 'image_button' render element for theme_input(). - * - * @param array $element - * An associative array containing the properties of the element. - * Properties used: #attributes, #button_type, #name, #value, #title, #src. - * - * The #button_type property accepts any value, though core themes have css that - * styles the following button_types appropriately: 'primary', 'danger'. - * - * @return array - * The $element with prepared variables ready for theme_input(). - */ -function form_pre_render_image_button($element) { - $element['#attributes']['type'] = 'image'; - Element::setAttributes($element, array('id', 'name', 'value')); - - $element['#attributes']['src'] = file_create_url($element['#src']); - if (!empty($element['#title'])) { - $element['#attributes']['alt'] = $element['#title']; - $element['#attributes']['title'] = $element['#title']; - } - - $element['#attributes']['class'][] = 'image-button'; - if (!empty($element['#button_type'])) { - $element['#attributes']['class'][] = 'image-button--' . $element['#button_type']; - } - // @todo Various JavaScript depends on this button class. - $element['#attributes']['class'][] = 'form-submit'; - - if (!empty($element['#attributes']['disabled'])) { - $element['#attributes']['class'][] = 'is-disabled'; - } - - return $element; -} - -/** - * Prepares a #type 'hidden' render element for theme_input(). - * - * @param array $element - * An associative array containing the properties of the element. - * Properties used: #name, #value, #attributes. - * - * @return array - * The $element with prepared variables ready for theme_input(). - */ -function form_pre_render_hidden($element) { - $element['#attributes']['type'] = 'hidden'; - Element::setAttributes($element, array('name', 'value')); - - return $element; -} - -/** - * Prepares a #type 'textfield' render element for theme_input(). - * - * @deprecated Use \Drupal\Core\Render\Element\Textfield::preRenderTextfield(). - */ -function form_pre_render_textfield($element) { - return Element\Textfield::preRenderTextfield($element); -} - -/** - * Prepares a #type 'email' render element for theme_input(). - * - * @param array $element - * An associative array containing the properties of the element. - * Properties used: #title, #value, #description, #size, #maxlength, - * #placeholder, #required, #attributes. - * - * @return array - * The $element with prepared variables ready for theme_input(). - */ -function form_pre_render_email($element) { - $element['#attributes']['type'] = 'email'; - Element::setAttributes($element, array('id', 'name', 'value', 'size', 'maxlength', 'placeholder')); - _form_set_attributes($element, array('form-email')); - - return $element; -} - -/** - * Form element validation handler for #type 'email'. - * - * Note that #maxlength and #required is validated by _form_validate() already. - */ -function form_validate_email(&$element, FormStateInterface $form_state) { - $value = trim($element['#value']); - form_set_value($element, $value, $form_state); - - if ($value !== '' && !valid_email_address($value)) { - form_error($element, $form_state, t('The email address %mail is not valid.', array('%mail' => $value))); - } -} - -/** - * Prepares a #type 'tel' render element for theme_input(). - * - * @param array $element - * An associative array containing the properties of the element. - * Properties used: #title, #value, #description, #size, #maxlength, - * #placeholder, #required, #attributes. - * - * @return array - * The $element with prepared variables ready for theme_input(). - */ -function form_pre_render_tel($element) { - $element['#attributes']['type'] = 'tel'; - Element::setAttributes($element, array('id', 'name', 'value', 'size', 'maxlength', 'placeholder')); - _form_set_attributes($element, array('form-tel')); - - return $element; -} - -/** - * Prepares a #type 'number' render element for theme_input(). - * - * @param array $element - * An associative array containing the properties of the element. - * Properties used: #title, #value, #description, #min, #max, #placeholder, - * #required, #attributes, #step, #size. - * - * @return array - * The $element with prepared variables ready for theme_input(). - */ -function form_pre_render_number($element) { - $element['#attributes']['type'] = 'number'; - Element::setAttributes($element, array('id', 'name', 'value', 'step', 'min', 'max', 'placeholder', 'size')); - _form_set_attributes($element, array('form-number')); - - return $element; -} - -/** - * Prepares a #type 'range' render element for theme_input(). - * - * @param array $element - * An associative array containing the properties of the element. - * Properties used: #title, #value, #description, #min, #max, #attributes, - * #step. - * - * @return array - * The $element with prepared variables ready for theme_input(). - */ -function form_pre_render_range($element) { - $element['#attributes']['type'] = 'range'; - Element::setAttributes($element, array('id', 'name', 'value', 'step', 'min', 'max')); - _form_set_attributes($element, array('form-range')); - - return $element; -} - -/** - * Form element validation handler for #type 'number'. - * - * Note that #required is validated by _form_validate() already. - */ -function form_validate_number(&$element, FormStateInterface $form_state) { - $value = $element['#value']; - if ($value === '') { - return; - } - - $name = empty($element['#title']) ? $element['#parents'][0] : $element['#title']; - - // Ensure the input is numeric. - if (!is_numeric($value)) { - form_error($element, $form_state, t('%name must be a number.', array('%name' => $name))); - return; - } - - // Ensure that the input is greater than the #min property, if set. - if (isset($element['#min']) && $value < $element['#min']) { - form_error($element, $form_state, t('%name must be higher than or equal to %min.', array('%name' => $name, '%min' => $element['#min']))); - } - - // Ensure that the input is less than the #max property, if set. - if (isset($element['#max']) && $value > $element['#max']) { - form_error($element, $form_state, t('%name must be lower than or equal to %max.', array('%name' => $name, '%max' => $element['#max']))); - } - - if (isset($element['#step']) && strtolower($element['#step']) != 'any') { - // Check that the input is an allowed multiple of #step (offset by #min if - // #min is set). - $offset = isset($element['#min']) ? $element['#min'] : 0.0; - - if (!Number::validStep($value, $element['#step'], $offset)) { - form_error($element, $form_state, t('%name is not a valid number.', array('%name' => $name))); - } - } -} - -/** - * Determines the value for a range element. - * - * Make sure range elements always have a value. The 'required' attribute is not - * allowed for range elements. - * - * @param $element - * The form element whose value is being populated. - * @param $input - * The incoming input to populate the form element. If this is FALSE, the - * element's default value should be returned. - * - * @return - * The data that will appear in the $form_state->getValues() collection for - * this element. Return nothing to use the default. - */ -function form_type_range_value($element, $input = FALSE) { - if ($input === '') { - $offset = ($element['#max'] - $element['#min']) / 2; - - // Round to the step. - if (strtolower($element['#step']) != 'any') { - $steps = round($offset / $element['#step']); - $offset = $element['#step'] * $steps; - } - - return $element['#min'] + $offset; - } -} - -/** - * Prepares a #type 'url' render element for theme_input(). - * - * @param array $element - * An associative array containing the properties of the element. - * Properties used: #title, #value, #description, #size, #maxlength, - * #placeholder, #required, #attributes. - * - * @return array - * The $element with prepared variables ready for theme_input(). - */ -function form_pre_render_url($element) { - $element['#attributes']['type'] = 'url'; - Element::setAttributes($element, array('id', 'name', 'value', 'size', 'maxlength', 'placeholder')); - _form_set_attributes($element, array('form-url')); - - return $element; -} - -/** - * Prepares a #type 'search' render element for theme_input(). - * - * @param array $element - * An associative array containing the properties of the element. - * Properties used: #title, #value, #description, #size, #maxlength, - * #placeholder, #required, #attributes. - * - * @return array - * The $element with prepared variables ready for theme_input(). - */ -function form_pre_render_search($element) { - $element['#attributes']['type'] = 'search'; - Element::setAttributes($element, array('id', 'name', 'value', 'size', 'maxlength', 'placeholder')); - _form_set_attributes($element, array('form-search')); - - return $element; -} - -/** - * Form element validation handler for #type 'url'. - * - * Note that #maxlength and #required is validated by _form_validate() already. - */ -function form_validate_url(&$element, FormStateInterface $form_state) { - $value = trim($element['#value']); - form_set_value($element, $value, $form_state); - - if ($value !== '' && !UrlHelper::isValid($value, TRUE)) { - form_error($element, $form_state, t('The URL %url is not valid.', array('%url' => $value))); - } -} - -/** - * Form element validation handler for #type 'color'. - */ -function form_validate_color(&$element, FormStateInterface $form_state) { - $value = trim($element['#value']); - - // Default to black if no value is given. - // @see http://www.w3.org/TR/html5/number-state.html#color-state - if ($value === '') { - form_set_value($element, '#000000', $form_state); - } - else { - // Try to parse the value and normalize it. - try { - form_set_value($element, Color::rgbToHex(Color::hexToRgb($value)), $form_state); - } - catch (InvalidArgumentException $e) { - form_error($element, $form_state, t('%name must be a valid color.', array('%name' => empty($element['#title']) ? $element['#parents'][0] : $element['#title']))); - } - } -} - -/** - * Prepares a #type 'color' render element for theme_input(). - * - * @param array $element - * An associative array containing the properties of the element. - * Properties used: #title, #value, #description, #attributes. - * - * @return array - * The $element with prepared variables ready for theme_input(). - */ -function form_pre_render_color($element) { - $element['#attributes']['type'] = 'color'; - Element::setAttributes($element, array('id', 'name', 'value')); - _form_set_attributes($element, array('form-color')); - - return $element; -} - -/** - * Prepares variables for form templates. - * - * Default template: form.html.twig. - * - * @param $variables - * An associative array containing: - * - element: An associative array containing the properties of the element. - * Properties used: #action, #method, #attributes, #children - */ -function template_preprocess_form(&$variables) { - $element = $variables['element']; - if (isset($element['#action'])) { - $element['#attributes']['action'] = UrlHelper::stripDangerousProtocols($element['#action']); - } - Element::setAttributes($element, array('method', 'id')); - if (empty($element['#attributes']['accept-charset'])) { - $element['#attributes']['accept-charset'] = "UTF-8"; - } - $variables['attributes'] = $element['#attributes']; - $variables['children'] = $element['#children']; -} - -/** - * Prepares variables for textarea templates. + * Prepares variables for textarea templates. * * Default template: textarea.html.twig. * @@ -2363,7 +593,7 @@ function template_preprocess_form(&$variables) { function template_preprocess_textarea(&$variables) { $element = $variables['element']; Element::setAttributes($element, array('id', 'name', 'rows', 'cols', 'placeholder')); - _form_set_attributes($element, array('form-textarea')); + Element\RenderElement::setAttributes($element, array('form-textarea')); $variables['wrapper_attributes'] = new Attribute(array( 'class' => array('form-textarea-wrapper'), )); @@ -2378,85 +608,6 @@ function template_preprocess_textarea(&$variables) { $variables['value'] = String::checkPlain($element['#value']); } -/** - * Prepares a #type 'password' render element for theme_input(). - * - * @param array $element - * An associative array containing the properties of the element. - * Properties used: #title, #value, #description, #size, #maxlength, - * #placeholder, #required, #attributes. - * - * @return array - * The $element with prepared variables ready for theme_input(). - */ -function form_pre_render_password($element) { - $element['#attributes']['type'] = 'password'; - Element::setAttributes($element, array('id', 'name', 'size', 'maxlength', 'placeholder')); - _form_set_attributes($element, array('form-text')); - - return $element; -} - -/** - * Expands a weight element into a select element. - */ -function form_process_weight($element) { - $element['#is_weight'] = TRUE; - - // If the number of options is small enough, use a select field. - $max_elements = \Drupal::config('system.site')->get('weight_select_max'); - if ($element['#delta'] <= $max_elements) { - $element['#type'] = 'select'; - for ($n = (-1 * $element['#delta']); $n <= $element['#delta']; $n++) { - $weights[$n] = $n; - } - $element['#options'] = $weights; - $element += element_info('select'); - } - // Otherwise, use a text field. - else { - $element['#type'] = 'number'; - // Use a field big enough to fit most weights. - $element['#size'] = 10; - $element += element_info('number'); - } - - return $element; -} - -/** - * Prepares a #type 'file' render element for theme_input(). - * - * For assistance with handling the uploaded file correctly, see the API - * provided by file.inc. - * - * @param array $element - * An associative array containing the properties of the element. - * Properties used: #title, #name, #size, #description, #required, - * #attributes. - * - * @return array - * The $element with prepared variables ready for theme_input(). - */ -function form_pre_render_file($element) { - $element['#attributes']['type'] = 'file'; - Element::setAttributes($element, array('id', 'name', 'size')); - _form_set_attributes($element, array('form-file')); - - return $element; -} - -/** - * Processes a file upload element, make use of #multiple if present. - */ -function form_process_file($element) { - if ($element['#multiple']) { - $element['#attributes'] = array('multiple' => 'multiple'); - $element['#name'] .= '[]'; - } - return $element; -} - /** * Returns HTML for a form element. * Prepares variables for form element templates. @@ -2486,10 +637,11 @@ function form_process_file($element) { * property hides the label for everyone except screen readers. * - attribute: Set the title attribute on the element to create a tooltip * but output no label element. This is supported only for checkboxes - * and radios in form_pre_render_conditional_form_element(). It is used - * where a visual label is not needed, such as a table of checkboxes where - * the row and column provide the context. The tooltip will include the - * title and required marker. + * and radios in + * \Drupal\Core\Render\Element\CompositeFormElementTrait::preRenderCompositeFormElement(). + * It is used where a visual label is not needed, such as a table of + * checkboxes where the row and column provide the context. The tooltip will + * include the title and required marker. * * If the #title property is not set, then the label and any required marker * will not be output, regardless of the #title_display or #required values. @@ -2631,37 +783,6 @@ function template_preprocess_form_element_label(&$variables) { } } -/** - * Sets a form element's class attribute. - * - * Adds 'required' and 'error' classes as needed. - * - * @param $element - * The form element. - * @param $name - * Array of new class names to be added. - */ -function _form_set_attributes(&$element, $class = array()) { - if (!empty($class)) { - if (!isset($element['#attributes']['class'])) { - $element['#attributes']['class'] = array(); - } - $element['#attributes']['class'] = array_merge($element['#attributes']['class'], $class); - } - // This function is invoked from form element theme functions, but the - // rendered form element may not necessarily have been processed by - // form_builder(). - if (!empty($element['#required'])) { - $element['#attributes']['class'][] = 'required'; - $element['#attributes']['required'] = 'required'; - $element['#attributes']['aria-required'] = 'true'; - } - if (isset($element['#parents']) && isset($element['#errors']) && !empty($element['#validated'])) { - $element['#attributes']['class'][] = 'error'; - $element['#attributes']['aria-invalid'] = 'true'; - } -} - /** * @defgroup batch Batch operations * @{ diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 15ef789bb7e9ed300e2dc0845df65f50bcbe8aac..0a6f475dedec2122892f301063d28fb8a8de2018 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -11,7 +11,6 @@ use Drupal\Component\Serialization\Json; use Drupal\Component\Utility\SafeMarkup; use Drupal\Component\Utility\String; -use Drupal\Component\Utility\UrlHelper; use Drupal\Component\Utility\Xss; use Drupal\Core\Config\Config; use Drupal\Core\Config\StorageException; @@ -801,25 +800,6 @@ function theme_disable($theme_list) { return \Drupal::service('theme_handler')->disable($theme_list); } -/** - * Renders a twig string directly. - * - * @param string $template_string - * The template string to render with placeholders. - * @param array $context - * An array of parameters to pass to the template. - * - * @return string - * The rendered inline template. - */ -function drupal_render_inline_template(&$element) { - /** @var \Drupal\Core\Template\TwigEnvironment $environment */ - $environment = \Drupal::service('twig'); - $markup = $environment->renderInline($element['#template'], $element['#context']); - $element['#markup'] = $markup; - return $element; -} - /** * @addtogroup themeable * @{ @@ -1088,129 +1068,6 @@ function template_preprocess_image(&$variables) { } } -/** - * #pre_render callback to transform children of an element into #rows suitable for theme_table(). - * - * This function converts sub-elements of an element of #type 'table' to be - * suitable for theme_table(): - * - The first level of sub-elements are table rows. Only the #attributes - * property is taken into account. - * - The second level of sub-elements is converted into columns for the - * corresponding first-level table row. - * - * Simple example usage: - * @code - * $form['table'] = array( - * '#type' => 'table', - * '#header' => array(t('Title'), array('data' => t('Operations'), 'colspan' => '1')), - * // Optionally, to add tableDrag support: - * '#tabledrag' => array( - * array( - * 'action' => 'order', - * 'relationship' => 'sibling', - * 'group' => 'thing-weight', - * ), - * ), - * ); - * foreach ($things as $row => $thing) { - * $form['table'][$row]['#weight'] = $thing['weight']; - * - * $form['table'][$row]['title'] = array( - * '#type' => 'textfield', - * '#default_value' => $thing['title'], - * ); - * - * // Optionally, to add tableDrag support: - * $form['table'][$row]['#attributes']['class'][] = 'draggable'; - * $form['table'][$row]['weight'] = array( - * '#type' => 'textfield', - * '#title' => t('Weight for @title', array('@title' => $thing['title'])), - * '#title_display' => 'invisible', - * '#size' => 4, - * '#default_value' => $thing['weight'], - * '#attributes' => array('class' => array('thing-weight')), - * ); - * - * // The amount of link columns should be identical to the 'colspan' - * // attribute in #header above. - * $form['table'][$row]['edit'] = array( - * '#type' => 'link', - * '#title' => t('Edit'), - * '#href' => 'thing/' . $row . '/edit', - * ); - * } - * @endcode - * - * @param array $element - * A structured array containing two sub-levels of elements. Properties used: - * - #tabledrag: The value is a list of $options arrays that are passed to - * drupal_attach_tabledrag(). The HTML ID of the table is added to each - * $options array. - * - * @see system_element_info() - * @see theme_table() - * @see drupal_process_attached() - * @see drupal_attach_tabledrag() - */ -function drupal_pre_render_table(array $element) { - foreach (Element::children($element) as $first) { - $row = array('data' => array()); - // Apply attributes of first-level elements as table row attributes. - if (isset($element[$first]['#attributes'])) { - $row += $element[$first]['#attributes']; - } - // Turn second-level elements into table row columns. - // @todo Do not render a cell for children of #type 'value'. - // @see http://drupal.org/node/1248940 - foreach (Element::children($element[$first]) as $second) { - // Assign the element by reference, so any potential changes to the - // original element are taken over. - $column = array('data' => &$element[$first][$second]); - - // Apply wrapper attributes of second-level elements as table cell - // attributes. - if (isset($element[$first][$second]['#wrapper_attributes'])) { - $column += $element[$first][$second]['#wrapper_attributes']; - } - - $row['data'][] = $column; - } - $element['#rows'][] = $row; - } - - // Take over $element['#id'] as HTML ID attribute, if not already set. - Element::setAttributes($element, array('id')); - - - // Add sticky headers, if applicable. - if (count($element['#header']) && $element['#sticky']) { - $element['#attached']['library'][] = 'core/drupal.tableheader'; - // Add 'sticky-enabled' class to the table to identify it for JS. - // This is needed to target tables constructed by this function. - $element['#attributes']['class'][] = 'sticky-enabled'; - } - // If the table has headers and it should react responsively to columns hidden - // with the classes represented by the constants RESPONSIVE_PRIORITY_MEDIUM - // and RESPONSIVE_PRIORITY_LOW, add the tableresponsive behaviors. - if (count($element['#header']) && $element['#responsive']) { - $element['#attached']['library'][] = 'core/drupal.tableresponsive'; - // Add 'responsive-enabled' class to the table to identify it for JS. - // This is needed to target tables constructed by this function. - $element['#attributes']['class'][] = 'responsive-enabled'; - } - - // If the custom #tabledrag is set and there is a HTML ID, add the table's - // HTML ID to the options and attach the behavior. - if (!empty($element['#tabledrag']) && isset($element['#attributes']['id'])) { - foreach ($element['#tabledrag'] as $options) { - $options['table_id'] = $element['#attributes']['id']; - drupal_attach_tabledrag($element, $options); - } - } - - return $element; -} - /** * Prepares variables for table templates. * @@ -1732,38 +1589,10 @@ function _template_preprocess_default_variables() { /** * #pre_render callback for the html element type. * - * @param array $element - * A structured array containing the html element type build properties. - * - * @see system_element_info() + * @deprecated Use \Drupal\Core\Render\Element\Html::preRenderHtml(). */ function drupal_pre_render_html(array $element) { - // Add favicon. - if (theme_get_setting('features.favicon')) { - $favicon = theme_get_setting('favicon.url'); - $type = theme_get_setting('favicon.mimetype'); - $element['#attached']['drupal_add_html_head_link'][][] = array( - 'rel' => 'shortcut icon', - 'href' => UrlHelper::stripDangerousProtocols($favicon), - 'type' => $type, - ); - } - - return $element; -} - -/** - * #pre_render callback for the page element type. - * - * @param array $element - * A structured array containing the page element type build properties. - * - * @see system_element_info() - */ -function drupal_pre_render_page(array $element) { - $element['#cache']['tags']['theme'] = \Drupal::theme()->getActiveTheme()->getName(); - $element['#cache']['tags']['theme_global_settings'] = TRUE; - return $element; + return Element\Html::preRenderHtml($element); } /** diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EmailItem.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EmailItem.php index 09c36b49c5c118984feb4d31ef2f4e65c30a79c2..139ea47b0c11dc56107122918ceed3e4919f2e6b 100644 --- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EmailItem.php +++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/EmailItem.php @@ -9,6 +9,7 @@ use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\Field\FieldItemBase; +use Drupal\Core\Render\Element\Email; use Drupal\Core\TypedData\DataDefinition; /** @@ -42,7 +43,7 @@ public static function schema(FieldStorageDefinitionInterface $field_definition) 'columns' => array( 'value' => array( 'type' => 'varchar', - 'length' => EMAIL_MAX_LENGTH, + 'length' => Email::EMAIL_MAX_LENGTH, 'not null' => FALSE, ), ), @@ -59,8 +60,8 @@ public function getConstraints() { $constraints[] = $constraint_manager->create('ComplexData', array( 'value' => array( 'Length' => array( - 'max' => EMAIL_MAX_LENGTH, - 'maxMessage' => t('%name: the email address can not be longer than @max characters.', array('%name' => $this->getFieldDefinition()->getLabel(), '@max' => EMAIL_MAX_LENGTH)), + 'max' => Email::EMAIL_MAX_LENGTH, + 'maxMessage' => t('%name: the email address can not be longer than @max characters.', array('%name' => $this->getFieldDefinition()->getLabel(), '@max' => Email::EMAIL_MAX_LENGTH)), ) ), )); diff --git a/core/lib/Drupal/Core/Render/Element/Actions.php b/core/lib/Drupal/Core/Render/Element/Actions.php new file mode 100644 index 0000000000000000000000000000000000000000..1aab7d1e16e5d16846f3744f9d72889b3b2bdc10 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Actions.php @@ -0,0 +1,109 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\Actions. + */ + +namespace Drupal\Core\Render\Element; + +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element; + +/** + * Provides a wrapper element to group one or more buttons in a form. + * + * Use of the 'actions' element as an array key helps to ensure proper styling + * in themes and to enable other modules to properly alter a form's actions. + * + * @RenderElement("actions") + */ +class Actions extends Container { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return array( + '#process' => array( + // @todo Move this to #pre_render. + array($class, 'preRenderActionsDropbutton'), + array($class, 'processActions'), + array($class, 'processContainer'), + ), + '#weight' => 100, + '#theme_wrappers' => array('container'), + ); + } + + /** + * Processes a form actions container element. + * + * @param array $element + * An associative array containing the properties and children of the + * form actions container. + * @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. + */ + public static function processActions(&$element, FormStateInterface $form_state, &$complete_form) { + $element['#attributes']['class'][] = 'form-actions'; + return $element; + } + + /** + * #pre_render callback for #type 'actions'. + * + * This callback iterates over all child elements of the #type 'actions' + * container to look for elements with a #dropbutton property, so as to group + * those elements into dropbuttons. As such, it works similar to #group, but is + * specialized for dropbuttons. + * + * The value of #dropbutton denotes the dropbutton to group the child element + * into. For example, two different values of 'foo' and 'bar' on child elements + * would generate two separate dropbuttons, which each contain the corresponding + * buttons. + * + * @param array $element + * The #type 'actions' element to process. + * @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 #type 'actions' element, including individual buttons grouped + * into new #type 'dropbutton' elements. + */ + public static function preRenderActionsDropbutton(&$element, FormStateInterface $form_state, &$complete_form) { + $dropbuttons = array(); + foreach (Element::children($element, TRUE) as $key) { + if (isset($element[$key]['#dropbutton'])) { + $dropbutton = $element[$key]['#dropbutton']; + // If there is no dropbutton for this button group yet, create one. + if (!isset($dropbuttons[$dropbutton])) { + $dropbuttons[$dropbutton] = array( + '#type' => 'dropbutton', + ); + } + // Add this button to the corresponding dropbutton. + // @todo Change #type 'dropbutton' to be based on theme_item_list() + // instead of links.html.twig to avoid this preemptive rendering. + $button = drupal_render($element[$key]); + $dropbuttons[$dropbutton]['#links'][$key] = array( + 'title' => $button, + 'html' => TRUE, + ); + } + } + // @todo For now, all dropbuttons appear first. Consider to invent a more + // fancy sorting/injection algorithm here. + return $dropbuttons + $element; + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/Ajax.php b/core/lib/Drupal/Core/Render/Element/Ajax.php new file mode 100644 index 0000000000000000000000000000000000000000..b916929f9185062d7f812c3ba316f7edc49d5cfb --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Ajax.php @@ -0,0 +1,36 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\Ajax. + */ + +namespace Drupal\Core\Render\Element; + +/** + * Provides a render element for adding Ajax to a render element. + * + * Holds an array whose values control the Ajax behavior of the element. + * + * @ingroup ajax + * + * @RenderElement("ajax") + */ +class Ajax extends RenderElement { + + /** + * {@inheritdoc} + */ + public function getInfo() { + // By default, we don't want Ajax commands being rendered in the context of + // an HTML page, so we don't provide defaults for #theme or #theme_wrappers. + // However, modules can set these properties (for example, to provide an + // HTML debugging page that displays rather than executes Ajax commands). + return array( + '#header' => TRUE, + '#commands' => array(), + '#error' => NULL, + ); + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/Button.php b/core/lib/Drupal/Core/Render/Element/Button.php new file mode 100644 index 0000000000000000000000000000000000000000..11a9093a48f524505ad81be186a49efb72e73878 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Button.php @@ -0,0 +1,89 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\Button. + */ + +namespace Drupal\Core\Render\Element; + +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element; + +/** + * Provides an action button form element. + * + * When the button is pressed, the form will be submitted to Drupal, where it is + * validated and rebuilt. The submit handler is not invoked. + * + * @FormElement("button") + */ +class Button extends FormElement { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return array( + '#input' => TRUE, + '#name' => 'op', + '#is_button' => TRUE, + '#executes_submit_callback' => FALSE, + '#limit_validation_errors' => FALSE, + '#process' => array( + array($class, 'processButton'), + array($class, 'processAjaxForm'), + ), + '#pre_render' => array( + array($class, 'preRenderButton'), + ), + '#theme_wrappers' => array('input__submit'), + ); + } + + /** + * Processes a form button element. + */ + public static function processButton(&$element, FormStateInterface $form_state, &$complete_form) { + // If this is a button intentionally allowing incomplete form submission + // (e.g., a "Previous" or "Add another item" button), then also skip + // client-side validation. + if (isset($element['#limit_validation_errors']) && $element['#limit_validation_errors'] !== FALSE) { + $element['#attributes']['formnovalidate'] = 'formnovalidate'; + } + return $element; + } + + /** + * Prepares a #type 'button' render element for theme_input(). + * + * @param array $element + * An associative array containing the properties of the element. + * Properties used: #attributes, #button_type, #name, #value. + * + * The #button_type property accepts any value, though core themes have CSS that + * styles the following button_types appropriately: 'primary', 'danger'. + * + * @return array + * The $element with prepared variables ready for theme_input(). + */ + public static function preRenderButton($element) { + $element['#attributes']['type'] = 'submit'; + Element::setAttributes($element, array('id', 'name', 'value')); + + $element['#attributes']['class'][] = 'button'; + if (!empty($element['#button_type'])) { + $element['#attributes']['class'][] = 'button--' . $element['#button_type']; + } + // @todo Various JavaScript depends on this button class. + $element['#attributes']['class'][] = 'form-submit'; + + if (!empty($element['#attributes']['disabled'])) { + $element['#attributes']['class'][] = 'is-disabled'; + } + + return $element; + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/Checkbox.php b/core/lib/Drupal/Core/Render/Element/Checkbox.php new file mode 100644 index 0000000000000000000000000000000000000000..43b37ca15b7f3d5078ce16deb47b35673657bcb4 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Checkbox.php @@ -0,0 +1,127 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\Checkbox. + */ + +namespace Drupal\Core\Render\Element; + +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element; + +/** + * Provides a form element for a single checkbox. + * + * @see \Drupal\Core\Render\Element\Checkboxes + * + * @FormElement("checkbox") + */ +class Checkbox extends FormElement { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return array( + '#input' => TRUE, + '#return_value' => 1, + '#process' => array( + array($class, 'processCheckbox'), + array($class, 'processAjaxForm'), + array($class, 'processGroup'), + ), + '#pre_render' => array( + array($class, 'preRenderCheckbox'), + array($class, 'preRenderGroup'), + ), + '#theme' => 'input__checkbox', + '#theme_wrappers' => array('form_element'), + '#title_display' => 'after', + ); + } + + /** + * {@inheritdoc} + */ + public static function valueCallback(&$element, $input, FormStateInterface $form_state) { + if ($input === FALSE) { + // Use #default_value as the default value of a checkbox, except change + // NULL to 0, because FormBuilder::handleInputElement() would otherwise + // replace NULL with empty string, but an empty string is a potentially + // valid value for a checked checkbox. + return isset($element['#default_value']) ? $element['#default_value'] : 0; + } + else { + // Checked checkboxes are submitted with a value (possibly '0' or ''): + // http://www.w3.org/TR/html401/interact/forms.html#successful-controls. + // For checked checkboxes, browsers submit the string version of + // #return_value, but we return the original #return_value. For unchecked + // checkboxes, browsers submit nothing at all, but + // FormBuilder::handleInputElement() detects this, and calls this + // function with $input=NULL. Returning NULL from a value callback means + // to use the default value, which is not what is wanted when an unchecked + // checkbox is submitted, so we use integer 0 as the value indicating an + // unchecked checkbox. Therefore, modules must not use integer 0 as a + // #return_value, as doing so results in the checkbox always being treated + // as unchecked. The string '0' is allowed for #return_value. The most + // common use-case for setting #return_value to either 0 or '0' is for the + // first option within a 0-indexed array of checkboxes, and for this, + // form_process_checkboxes() uses the string rather than the integer. + return isset($input) ? $element['#return_value'] : 0; + } + } + + /** + * Prepares a #type 'checkbox' render element for theme_input(). + * + * @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 theme_input(). + */ + public static function preRenderCheckbox($element) { + $element['#attributes']['type'] = 'checkbox'; + Element::setAttributes($element, array('id', 'name', '#return_value' => 'value')); + + // Unchecked checkbox has #value of integer 0. + if (!empty($element['#checked'])) { + $element['#attributes']['checked'] = 'checked'; + } + static::setAttributes($element, array('form-checkbox')); + + return $element; + } + + /** + * Sets the #checked property of a checkbox element. + */ + public static function processCheckbox(&$element, FormStateInterface $form_state, &$complete_form) { + $value = $element['#value']; + $return_value = $element['#return_value']; + // On form submission, the #value of an available and enabled checked + // checkbox is #return_value, and the #value of an available and enabled + // unchecked checkbox is integer 0. On not submitted forms, and for + // checkboxes with #access=FALSE or #disabled=TRUE, the #value is + // #default_value (integer 0 if #default_value is NULL). Most of the time, + // a string comparison of #value and #return_value is sufficient for + // determining the "checked" state, but a value of TRUE always means checked + // (even if #return_value is 'foo'), and a value of FALSE or integer 0 + // always means unchecked (even if #return_value is '' or '0'). + if ($value === TRUE || $value === FALSE || $value === 0) { + $element['#checked'] = (bool) $value; + } + else { + // Compare as strings, so that 15 is not considered equal to '15foo', but + // 1 is considered equal to '1'. This cast does not imply that either + // #value or #return_value is expected to be a string. + $element['#checked'] = ((string) $value === (string) $return_value); + } + return $element; + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/Checkboxes.php b/core/lib/Drupal/Core/Render/Element/Checkboxes.php new file mode 100644 index 0000000000000000000000000000000000000000..aa3b64c2eb817acc1f687d50fdf22e2091fd4020 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Checkboxes.php @@ -0,0 +1,116 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\Checkboxes. + */ + +namespace Drupal\Core\Render\Element; + +use Drupal\Core\Form\FormStateInterface; + +/** + * Provides a form element for a set of checkboxes. + * + * #options is an associative array, where the key is the #return_value of the + * checkbox and the value is displayed. The #options array cannot have a 0 key, + * as it would not be possible to discern checked and unchecked states. + * + * @see \Drupal\Core\Render\Element\Radios + * @see \Drupal\Core\Render\Element\Checkbox + * + * @FormElement("checkboxes") + */ +class Checkboxes extends FormElement { + + use CompositeFormElementTrait; + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return array( + '#input' => TRUE, + '#process' => array( + array($class, 'processCheckboxes'), + ), + '#pre_render' => array( + array($class, 'preRenderCompositeFormElement'), + ), + '#theme_wrappers' => array('checkboxes'), + ); + } + + /** + * Processes a checkboxes form element. + */ + public static function processCheckboxes(&$element, FormStateInterface $form_state, &$complete_form) { + $value = is_array($element['#value']) ? $element['#value'] : array(); + $element['#tree'] = TRUE; + if (count($element['#options']) > 0) { + if (!isset($element['#default_value']) || $element['#default_value'] == 0) { + $element['#default_value'] = array(); + } + $weight = 0; + foreach ($element['#options'] as $key => $choice) { + // Integer 0 is not a valid #return_value, so use '0' instead. + // @see form_type_checkbox_value(). + // @todo For Drupal 8, cast all integer keys to strings for consistency + // with form_process_radios(). + if ($key === 0) { + $key = '0'; + } + // 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' => 'checkbox', + '#title' => $choice, + '#return_value' => $key, + '#default_value' => isset($value[$key]) ? $key : NULL, + '#attributes' => $element['#attributes'], + '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL, + '#weight' => $weight, + ); + } + } + return $element; + } + + /** + * {@inheritdoc} + */ + public static function valueCallback(&$element, $input, FormStateInterface $form_state) { + if ($input === FALSE) { + $value = array(); + $element += array('#default_value' => array()); + foreach ($element['#default_value'] as $key) { + $value[$key] = $key; + } + return $value; + } + elseif (is_array($input)) { + // Programmatic form submissions use NULL to indicate that a checkbox + // should be unchecked; see drupal_form_submit(). We therefore remove all + // NULL elements from the array before constructing the return value, to + // simulate the behavior of web browsers (which do not send unchecked + // checkboxes to the server at all). This will not affect non-programmatic + // form submissions, since all values in \Drupal::request()->request are + // strings. + foreach ($input as $key => $value) { + if (!isset($value)) { + unset($input[$key]); + } + } + return array_combine($input, $input); + } + else { + return array(); + } + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/Color.php b/core/lib/Drupal/Core/Render/Element/Color.php new file mode 100644 index 0000000000000000000000000000000000000000..fa057998427a2e2a697191eb4e553da0136d8211 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Color.php @@ -0,0 +1,82 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\Color. + */ + +namespace Drupal\Core\Render\Element; + +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element; +use Drupal\Component\Utility\Color as ColorUtility; + +/** + * Provides a form element for choosing a color. + * + * @FormElement("color") + */ +class Color extends FormElement { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return array( + '#input' => TRUE, + '#process' => array( + array($class, 'processAjaxForm'), + ), + '#element_validate' => array( + array($class, 'validateColor'), + ), + '#pre_render' => array( + array($class, 'preRenderColor'), + ), + '#theme' => 'input__color', + '#theme_wrappers' => array('form_element'), + ); + } + + /** + * Form element validation handler for #type 'color'. + */ + public static function validateColor(&$element, FormStateInterface $form_state, &$complete_form) { + $value = trim($element['#value']); + + // Default to black if no value is given. + // @see http://www.w3.org/TR/html5/number-state.html#color-state + if ($value === '') { + $form_state->setValueForElement($element, '#000000'); + } + else { + // Try to parse the value and normalize it. + try { + $form_state->setValueForElement($element, ColorUtility::rgbToHex(ColorUtility::hexToRgb($value))); + } + catch (\InvalidArgumentException $e) { + $form_state->setError($element, t('%name must be a valid color.', array('%name' => empty($element['#title']) ? $element['#parents'][0] : $element['#title']))); + } + } + } + + /** + * Prepares a #type 'color' render element for theme_input(). + * + * @param array $element + * An associative array containing the properties of the element. + * Properties used: #title, #value, #description, #attributes. + * + * @return array + * The $element with prepared variables ready for theme_input(). + */ + public static function preRenderColor($element) { + $element['#attributes']['type'] = 'color'; + Element::setAttributes($element, array('id', 'name', 'value')); + static::setAttributes($element, array('form-color')); + + return $element; + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/CompositeFormElementTrait.php b/core/lib/Drupal/Core/Render/Element/CompositeFormElementTrait.php new file mode 100644 index 0000000000000000000000000000000000000000..4c1855aca27dd966d63044b306105f9bd95d0e51 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/CompositeFormElementTrait.php @@ -0,0 +1,42 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\CompositeFormElementTrait. + */ + +namespace Drupal\Core\Render\Element; + +/** + * Provides a trait for radios, checkboxes, and similar composite form elements. + * + * Any form element that is comprised of several distinct parts can use this + * trait to add support for a composite title or description. + */ +trait CompositeFormElementTrait { + + /** + * Adds form element theming to an element if its title or description is set. + * + * This is used as a pre render function for checkboxes and radios. + */ + public static function preRenderCompositeFormElement($element) { + // Set the element's title attribute to show #title as a tooltip, if needed. + if (isset($element['#title']) && $element['#title_display'] == 'attribute') { + $element['#attributes']['title'] = $element['#title']; + if (!empty($element['#required'])) { + // Append an indication that this field is required. + $element['#attributes']['title'] .= ' (' . t('Required') . ')'; + } + } + + if (isset($element['#title']) || isset($element['#description'])) { + // @see #type 'fieldgroup' + $element['#theme_wrappers'][] = 'fieldset'; + $element['#attributes']['class'][] = 'fieldgroup'; + $element['#attributes']['class'][] = 'form-composite'; + } + return $element; + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/Container.php b/core/lib/Drupal/Core/Render/Element/Container.php new file mode 100644 index 0000000000000000000000000000000000000000..3fda9baa8a6bf75586be57a35f11cc1522cb9141 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Container.php @@ -0,0 +1,61 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\Container. + */ + +namespace Drupal\Core\Render\Element; + +use Drupal\Core\Form\FormStateInterface; + +/** + * Provides a render element that wraps child elements in a container. + * + * Surrounds child elements with a <div> and adds attributes such as classes or + * an HTML ID. + * + * @RenderElement("container") + */ +class Container extends RenderElement { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return array( + '#process' => array( + array($class, 'processGroup'), + array($class, 'processContainer'), + ), + '#pre_render' => array( + array($class, 'preRenderGroup'), + ), + '#theme_wrappers' => array('container'), + ); + } + + /** + * Processes a container element. + * + * @param array $element + * An associative array containing the properties and children of the + * container. + * @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. + */ + public static function processContainer(&$element, FormStateInterface $form_state, &$complete_form) { + // Generate the ID of the element if it's not explicitly given. + if (!isset($element['#id'])) { + $element['#id'] = drupal_html_id(implode('-', $element['#parents']) . '-wrapper'); + } + return $element; + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/Date.php b/core/lib/Drupal/Core/Render/Element/Date.php new file mode 100644 index 0000000000000000000000000000000000000000..8585cb34e867cc525c4dedb32ef3c4b2a0081c3d --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Date.php @@ -0,0 +1,68 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\Date. + */ + +namespace Drupal\Core\Render\Element; + +use Drupal\Core\Render\Element; + +/** + * Provides a form element for date selection. + * + * The #default_value will be today's date if no value is supplied. The format + * for the #default_value and the #return_value is an array with three elements + * with the keys: 'year', month', and 'day'. For example, + * array('year' => 2007, 'month' => 2, 'day' => 15) + * + * @FormElement("date") + */ +class Date extends FormElement { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return array( + '#input' => TRUE, + '#theme' => 'input__date', + '#pre_render' => array( + array($class, 'preRenderDate'), + ), + '#theme_wrappers' => array('form_element'), + ); + } + + /** + * Adds form-specific attributes to a 'date' #type element. + * + * Supports HTML5 types of 'date', 'datetime', 'datetime-local', and 'time'. + * Falls back to a plain textfield. Used as a sub-element by the datetime + * element type. + * + * @param array $element + * An associative array containing the properties of the element. + * Properties used: #title, #value, #options, #description, #required, + * #attributes, #id, #name, #type, #min, #max, #step, #value, #size. + * + * Note: The input "name" attribute needs to be sanitized before output, which + * is currently done by initializing Drupal\Core\Template\Attribute with + * all the attributes. + * + * @return array + * The $element with prepared variables ready for #theme 'input__date'. + */ + public static function preRenderDate($element) { + if (empty($element['#attributes']['type'])) { + $element['#attributes']['type'] = 'date'; + } + Element::setAttributes($element, array('id', 'name', 'type', 'min', 'max', 'step', 'value', 'size')); + static::setAttributes($element, array('form-' . $element['#attributes']['type'])); + + return $element; + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/Details.php b/core/lib/Drupal/Core/Render/Element/Details.php new file mode 100644 index 0000000000000000000000000000000000000000..c45810ef2dfc9c1e53ea13163bd55ad39871132d --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Details.php @@ -0,0 +1,78 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\Details. + */ + +namespace Drupal\Core\Render\Element; + +use Drupal\Core\Render\Element; + +/** + * Provides a render element for a details element, similar to a fieldset. + * + * Fieldsets can only be used in forms, while details elements can be used + * outside of forms. + * + * @see \Drupal\Core\Render\Element\Fieldset + * + * @RenderElement("details") + */ +class Details extends RenderElement { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return array( + '#open' => FALSE, + '#value' => NULL, + '#process' => array( + array($class, 'processGroup'), + array($class, 'processAjaxForm'), + ), + '#pre_render' => array( + array($class, 'preRenderDetails'), + array($class, 'preRenderGroup'), + ), + '#theme_wrappers' => array('details'), + ); + } + + /** + * Adds form element theming to details. + * + * @param $element + * An associative array containing the properties and children of the + * details. + * + * @return + * The modified element. + */ + public static function preRenderDetails($element) { + Element::setAttributes($element, array('id')); + + // The .form-wrapper class is required for #states to treat details like + // containers. + static::setAttributes($element, array('form-wrapper')); + + // Collapsible details. + $element['#attached']['library'][] = 'core/drupal.collapse'; + if (!empty($element['#open'])) { + $element['#attributes']['open'] = 'open'; + } + + // Do not render optional details elements if there are no children. + if (isset($element['#parents'])) { + $group = implode('][', $element['#parents']); + if (!empty($element['#optional']) && !Element::getVisibleChildren($element['#groups'][$group])) { + $element['#printed'] = TRUE; + } + } + + return $element; + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/Dropbutton.php b/core/lib/Drupal/Core/Render/Element/Dropbutton.php new file mode 100644 index 0000000000000000000000000000000000000000..76fab8960d468224e72fb6e046b134717149efc2 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Dropbutton.php @@ -0,0 +1,52 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\Dropbutton. + */ + +namespace Drupal\Core\Render\Element; + +/** + * Provides a render element for a set of links rendered as a drop-down button. + * + * @see \Drupal\Core\Render\Element\Operations + * + * @RenderElement("dropbutton") + */ +class Dropbutton extends RenderElement { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return array( + '#pre_render' => array( + array($class, 'preRenderDropbutton'), + ), + '#theme' => 'links__dropbutton', + ); + } + + /** + * Pre-render callback: Attaches the dropbutton library and required markup. + */ + public static function preRenderDropbutton($element) { + $element['#attached']['library'][] = 'core/drupal.dropbutton'; + $element['#attributes']['class'][] = 'dropbutton'; + if (!isset($element['#theme_wrappers'])) { + $element['#theme_wrappers'] = array(); + } + array_unshift($element['#theme_wrappers'], 'dropbutton_wrapper'); + + // Enable targeted theming of specific dropbuttons (e.g., 'operations' or + // 'operations__node'). + if (isset($element['#subtype'])) { + $element['#theme'] .= '__' . $element['#subtype']; + } + + return $element; + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/ElementInterface.php b/core/lib/Drupal/Core/Render/Element/ElementInterface.php index a1035f7b2b76a8b25ea4c9a3f684bfe6b93c23c2..61a05a3e1594df93fa8107f7fdfba93cfc571d27 100644 --- a/core/lib/Drupal/Core/Render/Element/ElementInterface.php +++ b/core/lib/Drupal/Core/Render/Element/ElementInterface.php @@ -10,7 +10,7 @@ use Drupal\Component\Plugin\PluginInspectionInterface; /** - * Provides an interface for element plugins. + * Provides an interface for render element plugins. * * Render element plugins allow modules to declare their own Render API element * types and specify the default values for the properties. The values returned @@ -42,4 +42,16 @@ interface ElementInterface extends PluginInspectionInterface { */ public function getInfo(); + /** + * Sets a form element's class attribute. + * + * Adds 'required' and 'error' classes as needed. + * + * @param array $element + * The form element. + * @param array $class + * Array of new class names to be added. + */ + public static function setAttributes(&$element, $class = array()); + } diff --git a/core/lib/Drupal/Core/Render/Element/Email.php b/core/lib/Drupal/Core/Render/Element/Email.php new file mode 100644 index 0000000000000000000000000000000000000000..c17633f3146340e1bb156498798bdbd4382bb676 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Email.php @@ -0,0 +1,90 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\Email. + */ + +namespace Drupal\Core\Render\Element; + +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element; + +/** + * Provides a form input element for entering an email address. + * + * @FormElement("email") + */ +class Email extends FormElement { + + /** + * Defines the max length for an email address + * + * The maximum length of an email address is 254 characters. RFC 3696 + * specifies a total length of 320 characters, but mentions that + * addresses longer than 256 characters are not normally useful. Erratum + * 1690 was then released which corrected this value to 254 characters. + * @see http://tools.ietf.org/html/rfc3696#section-3 + * @see http://www.rfc-editor.org/errata_search.php?rfc=3696&eid=1690 + */ + const EMAIL_MAX_LENGTH = 254; + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return array( + '#input' => TRUE, + '#size' => 60, + '#maxlength' => self::EMAIL_MAX_LENGTH, + '#autocomplete_route_name' => FALSE, + '#process' => array( + array($class, 'processAutocomplete'), + array($class, 'processAjaxForm'), + array($class, 'processPattern'), + ), + '#element_validate' => array( + array($class, 'validateEmail'), + ), + '#pre_render' => array( + array($class, 'preRenderEmail'), + ), + '#theme' => 'input__email', + '#theme_wrappers' => array('form_element'), + ); + } + + /** + * Form element validation handler for #type 'email'. + * + * Note that #maxlength and #required is validated by _form_validate() already. + */ + public static function validateEmail(&$element, FormStateInterface $form_state, &$complete_form) { + $value = trim($element['#value']); + $form_state->setValueForElement($element, $value); + + if ($value !== '' && !valid_email_address($value)) { + $form_state->setError($element, t('The email address %mail is not valid.', array('%mail' => $value))); + } + } + + /** + * Prepares a #type 'email' render element for theme_input(). + * + * @param array $element + * An associative array containing the properties of the element. + * Properties used: #title, #value, #description, #size, #maxlength, + * #placeholder, #required, #attributes. + * + * @return array + * The $element with prepared variables ready for theme_input(). + */ + public static function preRenderEmail($element) { + $element['#attributes']['type'] = 'email'; + Element::setAttributes($element, array('id', 'name', 'value', 'size', 'maxlength', 'placeholder')); + static::setAttributes($element, array('form-email')); + return $element; + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/Fieldgroup.php b/core/lib/Drupal/Core/Render/Element/Fieldgroup.php new file mode 100644 index 0000000000000000000000000000000000000000..56840b88fa3d9642a84555d7e6f39b403aff1121 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Fieldgroup.php @@ -0,0 +1,29 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\Fieldgroup. + */ + +namespace Drupal\Core\Render\Element; + +/** + * Provides a render element for a group of form elements. + * + * In default rendering, the only difference between a 'fieldgroup' and a + * 'fieldset' is the CSS class applied to the containing HTML element. + * + * @see \Drupal\Core\Render\Element\Fieldset + * @see \Drupal\Core\Render\Element\Details + * + * @RenderElement("fieldgroup") + */ +class Fieldgroup extends Fieldset { + + public function getInfo() { + return array( + '#attributes' => array('class' => array('fieldgroup')), + ) + parent::getInfo(); + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/Fieldset.php b/core/lib/Drupal/Core/Render/Element/Fieldset.php new file mode 100644 index 0000000000000000000000000000000000000000..9ef2070467bc15b42a4ccb0aed24501fee6afb90 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Fieldset.php @@ -0,0 +1,43 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\Fieldset. + */ + +namespace Drupal\Core\Render\Element; + +use Drupal\Core\Render\Element; + +/** + * Provides a render element for a group of form elements. + * + * In default rendering, the only difference between a 'fieldgroup' and a + * 'fieldset' is the CSS class applied to the containing HTML element. + * + * @see \Drupal\Core\Render\Element\Fieldgroup + * @see \Drupal\Core\Render\Element\Details + * + * @RenderElement("fieldset") + */ +class Fieldset extends RenderElement { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return array( + '#process' => array( + array($class, 'processGroup'), + array($class, 'processAjaxForm'), + ), + '#pre_render' => array( + array($class, 'preRenderGroup'), + ), + '#value' => NULL, + '#theme_wrappers' => array('fieldset'), + ); + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/File.php b/core/lib/Drupal/Core/Render/Element/File.php new file mode 100644 index 0000000000000000000000000000000000000000..848e401a0ec353a00ce88692852d2d7ebe52d8e1 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/File.php @@ -0,0 +1,73 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\File. + */ + +namespace Drupal\Core\Render\Element; + +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element; + +/** + * Provides a form element for uploading a file. + * + * @FormElement("file") + */ +class File extends FormElement { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return array( + '#input' => TRUE, + '#multiple' => FALSE, + '#process' => array( + array($class, 'processFile'), + ), + '#size' => 60, + '#pre_render' => array( + array($class, 'preRenderFile'), + ), + '#theme' => 'input__file', + '#theme_wrappers' => array('form_element'), + ); + } + + /** + * Processes a file upload element, make use of #multiple if present. + */ + public static function processFile(&$element, FormStateInterface $form_state, &$complete_form) { + if ($element['#multiple']) { + $element['#attributes'] = array('multiple' => 'multiple'); + $element['#name'] .= '[]'; + } + return $element; + } + + /** + * Prepares a #type 'file' render element for theme_input(). + * + * For assistance with handling the uploaded file correctly, see the API + * provided by file.inc. + * + * @param array $element + * An associative array containing the properties of the element. + * Properties used: #title, #name, #size, #description, #required, + * #attributes. + * + * @return array + * The $element with prepared variables ready for theme_input(). + */ + public static function preRenderFile($element) { + $element['#attributes']['type'] = 'file'; + Element::setAttributes($element, array('id', 'name', 'size')); + static::setAttributes($element, array('form-file')); + + return $element; + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/Form.php b/core/lib/Drupal/Core/Render/Element/Form.php new file mode 100644 index 0000000000000000000000000000000000000000..2dbd1c77c67c1b8b3af9f56792c357419aa2e2c2 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Form.php @@ -0,0 +1,30 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\Form. + */ + +namespace Drupal\Core\Render\Element; + +use Drupal\Core\Render\Element; + +/** + * Provides a render element for a form. + * + * @RenderElement("form") + */ +class Form extends RenderElement { + + /** + * {@inheritdoc} + */ + public function getInfo() { + return array( + '#method' => 'post', + '#action' => request_uri(), + '#theme_wrappers' => array('form'), + ); + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/FormElement.php b/core/lib/Drupal/Core/Render/Element/FormElement.php index d66cc89d6eaee4812108985a66ed1a4c3491751f..401c85056f53e5eb8ee771e8973ee26b186eb471 100644 --- a/core/lib/Drupal/Core/Render/Element/FormElement.php +++ b/core/lib/Drupal/Core/Render/Element/FormElement.php @@ -10,7 +10,7 @@ use Drupal\Core\Form\FormStateInterface; /** - * Provides a base class for form render plugins. + * Provides a base class for form element plugins. * * @see \Drupal\Core\Render\Annotation\FormElement * @see \Drupal\Core\Render\Element\FormElementInterface @@ -29,35 +29,11 @@ public static function valueCallback(&$element, $input, FormStateInterface $form } /** - * Form element processing handler for the #ajax form property. - * - * @param array $element - * An associative array containing the properties of the 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 ajax_pre_render_element() - */ - public static function processAjaxForm(&$element, FormStateInterface $form_state, &$complete_form) { - $element = ajax_pre_render_element($element); - if (!empty($element['#ajax_processed'])) { - $form_state['cache'] = TRUE; - } - return $element; - } - - /** - * Arranges elements into groups. + * #process callback for #pattern form element property. * * @param array $element * An associative array containing the properties and children of the - * element. Note that $element must be taken by reference here, so processed - * child elements are taken over into $form_state. + * generic input element. * @param \Drupal\Core\Form\FormStateInterface $form_state * The current state of the form. * @param array $complete_form @@ -66,47 +42,40 @@ public static function processAjaxForm(&$element, FormStateInterface $form_state * @return array * The processed element. */ - public static function processGroup(&$element, FormStateInterface $form_state, &$complete_form) { - $parents = implode('][', $element['#parents']); - - // Each details element forms a new group. The #type 'vertical_tabs' basically - // only injects a new details element. - $form_state['groups'][$parents]['#group_exists'] = TRUE; - $element['#groups'] = &$form_state['groups']; - - // Process vertical tabs group member details elements. - if (isset($element['#group'])) { - // Add this details element to the defined group (by reference). - $group = $element['#group']; - $form_state['groups'][$group][] = &$element; + public static function processPattern(&$element, FormStateInterface $form_state, &$complete_form) { + if (isset($element['#pattern']) && !isset($element['#attributes']['pattern'])) { + $element['#attributes']['pattern'] = $element['#pattern']; + $element['#element_validate'][] = array(get_called_class(), 'validatePattern'); } return $element; } /** - * #process callback for #pattern form element property. + * #element_validate callback for #pattern form element property. * - * @param array $element + * @param $element * An associative array containing the properties and children of the - * generic input element. - * @param \Drupal\Core\Form\FormStateInterface $form_state + * generic form element. + * @param $form_state * The current state of the form. * @param array $complete_form * The complete form structure. - * - * @return array - * The processed element. - * - * @see form_validate_pattern() */ - public static function processPattern(&$element, FormStateInterface $form_state, &$complete_form) { - if (isset($element['#pattern']) && !isset($element['#attributes']['pattern'])) { - $element['#attributes']['pattern'] = $element['#pattern']; - $element['#element_validate'][] = 'form_validate_pattern'; + public static function validatePattern(&$element, FormStateInterface $form_state, &$complete_form) { + if ($element['#value'] !== '') { + // The pattern must match the entire string and should have the same + // behavior as the RegExp object in ECMA 262. + // - Use bracket-style delimiters to avoid introducing a special delimiter + // character like '/' that would have to be escaped. + // - Put in brackets so that the pattern can't interfere with what's + // prepended and appended. + $pattern = '{^(?:' . $element['#pattern'] . ')$}'; + + if (!preg_match($pattern, $element['#value'])) { + $form_state->setError($element, t('%name field is not in the right format.', array('%name' => $element['#title']))); + } } - - return $element; } /** diff --git a/core/lib/Drupal/Core/Render/Element/Hidden.php b/core/lib/Drupal/Core/Render/Element/Hidden.php new file mode 100644 index 0000000000000000000000000000000000000000..bb955afb0f39cd064596d9e4e78ec400fbcbf85f --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Hidden.php @@ -0,0 +1,55 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\Hidden. + */ + +namespace Drupal\Core\Render\Element; + +use Drupal\Core\Render\Element; + +/** + * Provides a form element for an HTML 'hidden' input element. + * + * @see \Drupal\Core\Render\Element\Value + * + * @FormElement("hidden") + */ +class Hidden extends FormElement { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return array( + '#input' => TRUE, + '#process' => array( + array($class, 'processAjaxForm'), + ), + '#pre_render' => array( + array($class, 'preRenderHidden'), + ), + '#theme' => 'input__hidden', + ); + } + + /** + * Prepares a #type 'hidden' render element for theme_input(). + * + * @param array $element + * An associative array containing the properties of the element. + * Properties used: #name, #value, #attributes. + * + * @return array + * The $element with prepared variables ready for theme_input(). + */ + public static function preRenderHidden($element) { + $element['#attributes']['type'] = 'hidden'; + Element::setAttributes($element, array('name', 'value')); + + return $element; + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/Html.php b/core/lib/Drupal/Core/Render/Element/Html.php new file mode 100644 index 0000000000000000000000000000000000000000..58c0b547c99635a8461dcf0b8da2b3520609d519 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Html.php @@ -0,0 +1,67 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\Html. + */ + +namespace Drupal\Core\Render\Element; + +use Drupal\Component\Utility\UrlHelper; + +/** + * Provides a render element for <html>. + * + * @RenderElement("html") + */ +class Html extends RenderElement { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return array( + '#theme' => 'html', + '#pre_render' => array( + array($class, 'preRenderHtml'), + ), + // HTML5 Shiv + '#attached' => array( + 'library' => array('core/html5shiv'), + ), + ); + } + + /** + * #pre_render callback for the html element type. + * + * @param array $element + * A structured array containing the html element type build properties. + * + * @return array + * The processed element. + */ + public static function preRenderHtml($element) { + // Add favicon. + if (static::themeGetSetting('features.favicon')) { + $favicon = static::themeGetSetting('favicon.url'); + $type = static::themeGetSetting('favicon.mimetype'); + $element['#attached']['drupal_add_html_head_link'][][] = array( + 'rel' => 'shortcut icon', + 'href' => UrlHelper::stripDangerousProtocols($favicon), + 'type' => $type, + ); + } + + return $element; + } + + /** + * Wraps theme_get_setting(). + */ + protected static function themeGetSetting($setting_name) { + return theme_get_setting($setting_name); + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/HtmlTag.php b/core/lib/Drupal/Core/Render/Element/HtmlTag.php new file mode 100644 index 0000000000000000000000000000000000000000..851bbf9dfcbc3c6a107d8fbc61a568a12d72dbd4 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/HtmlTag.php @@ -0,0 +1,168 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\HtmlTag. + */ + +namespace Drupal\Core\Render\Element; + +use Drupal\Component\Utility\SafeMarkup; +use Drupal\Core\Template\Attribute; + +/** + * Provides a render element for any HTML tag, with properties and value. + * + * @RenderElement("html_tag") + */ +class HtmlTag extends RenderElement { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return array( + '#pre_render' => array( + array($class, 'preRenderConditionalComments'), + array($class, 'preRenderHtmlTag'), + ), + '#attributes' => array(), + '#value' => NULL, + ); + } + + /** + * Pre-render callback: Renders a generic HTML tag with attributes into #markup. + * + * Note: It is the caller's responsibility to sanitize any input parameters. + * This callback does not perform sanitization. + * + * @param array $element + * An associative array containing: + * - #tag: The tag name to output. Typical tags added to the HTML HEAD: + * - meta: To provide meta information, such as a page refresh. + * - link: To refer to stylesheets and other contextual information. + * - script: To load JavaScript. + * The value of #tag is not escaped or sanitized, so do not pass in user + * input. + * - #attributes: (optional) An array of HTML attributes to apply to the + * tag. + * - #value: (optional) A string containing tag content, such as inline + * CSS. + * - #value_prefix: (optional) A string to prepend to #value, e.g. a CDATA + * wrapper prefix. + * - #value_suffix: (optional) A string to append to #value, e.g. a CDATA + * wrapper suffix. + * + * @return array + */ + public static function preRenderHtmlTag($element) { + $attributes = isset($element['#attributes']) ? new Attribute($element['#attributes']) : ''; + if (!isset($element['#value'])) { + // This function is intended for internal use, so we assume that no unsafe + // values are passed in #tag. The attributes are already safe because + // Attribute output is already automatically sanitized. + // @todo Escape this properly instead? https://www.drupal.org/node/2296101 + $markup = SafeMarkup::set('<' . $element['#tag'] . $attributes . " />\n"); + } + else { + $markup = '<' . $element['#tag'] . $attributes . '>'; + if (isset($element['#value_prefix'])) { + $markup .= $element['#value_prefix']; + } + $markup .= $element['#value']; + if (isset($element['#value_suffix'])) { + $markup .= $element['#value_suffix']; + } + $markup .= '</' . $element['#tag'] . ">\n"; + // @todo We cannot actually guarantee this markup is safe. Consider a fix + // in: https://www.drupal.org/node/2296101 + $markup = SafeMarkup::set($markup); + } + if (!empty($element['#noscript'])) { + $element['#markup'] = '<noscript>' . $markup . '</noscript>'; + } + else { + $element['#markup'] = $markup; + } + return $element; + } + + /** + * Pre-render callback: Renders #browsers into #prefix and #suffix. + * + * @param array $element + * A render array with a '#browsers' property. The '#browsers' property can + * contain any or all of the following keys: + * - 'IE': If FALSE, the element is not rendered by Internet Explorer. If + * TRUE, the element is rendered by Internet Explorer. Can also be a string + * containing an expression for Internet Explorer to evaluate as part of a + * conditional comment. For example, this can be set to 'lt IE 7' for the + * element to be rendered in Internet Explorer 6, but not in Internet + * Explorer 7 or higher. Defaults to TRUE. + * - '!IE': If FALSE, the element is not rendered by browsers other than + * Internet Explorer. If TRUE, the element is rendered by those browsers. + * Defaults to TRUE. + * Examples: + * - To render an element in all browsers, '#browsers' can be left out or set + * to array('IE' => TRUE, '!IE' => TRUE). + * - To render an element in Internet Explorer only, '#browsers' can be set + * to array('!IE' => FALSE). + * - To render an element in Internet Explorer 6 only, '#browsers' can be set + * to array('IE' => 'lt IE 7', '!IE' => FALSE). + * - To render an element in Internet Explorer 8 and higher and in all other + * browsers, '#browsers' can be set to array('IE' => 'gte IE 8'). + * + * @return array + * The passed-in element with markup for conditional comments potentially + * added to '#prefix' and '#suffix'. + */ + public static function preRenderConditionalComments($element) { + $browsers = isset($element['#browsers']) ? $element['#browsers'] : array(); + $browsers += array( + 'IE' => TRUE, + '!IE' => TRUE, + ); + + // If rendering in all browsers, no need for conditional comments. + if ($browsers['IE'] === TRUE && $browsers['!IE']) { + return $element; + } + + // Determine the conditional comment expression for Internet Explorer to + // evaluate. + if ($browsers['IE'] === TRUE) { + $expression = 'IE'; + } + elseif ($browsers['IE'] === FALSE) { + $expression = '!IE'; + } + else { + $expression = $browsers['IE']; + } + + // Wrap the element's potentially existing #prefix and #suffix properties with + // conditional comment markup. The conditional comment expression is evaluated + // by Internet Explorer only. To control the rendering by other browsers, + // either the "downlevel-hidden" or "downlevel-revealed" technique must be + // used. See http://en.wikipedia.org/wiki/Conditional_comment for details. + $element += array( + '#prefix' => '', + '#suffix' => '', + ); + if (!$browsers['!IE']) { + // "downlevel-hidden". + $element['#prefix'] = "\n<!--[if $expression]>\n" . $element['#prefix']; + $element['#suffix'] .= "<![endif]-->\n"; + } + else { + // "downlevel-revealed". + $element['#prefix'] = "\n<!--[if $expression]><!-->\n" . $element['#prefix']; + $element['#suffix'] .= "<!--<![endif]-->\n"; + } + + return $element; + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/ImageButton.php b/core/lib/Drupal/Core/Render/Element/ImageButton.php new file mode 100644 index 0000000000000000000000000000000000000000..55177925388d6149e87bc64e06a0ff70807f3b09 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/ImageButton.php @@ -0,0 +1,98 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\ImageButton. + */ + +namespace Drupal\Core\Render\Element; + +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element; + +/** + * Provides a form element for a submit button with an image. + * + * @FormElement("image_button") + */ +class ImageButton extends Submit { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $info = parent::getInfo(); + unset($info['name']); + + return array( + '#return_value' => TRUE, + '#has_garbage_value' => TRUE, + '#src' => NULL, + '#theme_wrappers' => array('input__image_button'), + ) + $info; + } + + /** + * {@inheritdoc} + */ + public static function valueCallback(&$element, $input, FormStateInterface $form_state) { + if ($input !== FALSE) { + if (!empty($input)) { + // If we're dealing with Mozilla or Opera, we're lucky. It will + // return a proper value, and we can get on with things. + return $element['#return_value']; + } + else { + // Unfortunately, in IE we never get back a proper value for THIS + // form element. Instead, we get back two split values: one for the + // X and one for the Y coordinates on which the user clicked the + // button. We'll find this element in the #post data, and search + // in the same spot for its name, with '_x'. + $input = $form_state->getUserInput(); + foreach (explode('[', $element['#name']) as $element_name) { + // chop off the ] that may exist. + if (substr($element_name, -1) == ']') { + $element_name = substr($element_name, 0, -1); + } + + if (!isset($input[$element_name])) { + if (isset($input[$element_name . '_x'])) { + return $element['#return_value']; + } + return NULL; + } + $input = $input[$element_name]; + } + return $element['#return_value']; + } + } + } + + /** + * {@inheritdoc} + */ + public static function preRenderButton($element) { + $element['#attributes']['type'] = 'image'; + Element::setAttributes($element, array('id', 'name', 'value')); + + $element['#attributes']['src'] = file_create_url($element['#src']); + if (!empty($element['#title'])) { + $element['#attributes']['alt'] = $element['#title']; + $element['#attributes']['title'] = $element['#title']; + } + + $element['#attributes']['class'][] = 'image-button'; + if (!empty($element['#button_type'])) { + $element['#attributes']['class'][] = 'image-button--' . $element['#button_type']; + } + // @todo Various JavaScript depends on this button class. + $element['#attributes']['class'][] = 'form-submit'; + + if (!empty($element['#attributes']['disabled'])) { + $element['#attributes']['class'][] = 'is-disabled'; + } + + return $element; + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/InlineTemplate.php b/core/lib/Drupal/Core/Render/Element/InlineTemplate.php new file mode 100644 index 0000000000000000000000000000000000000000..7d2d553edd818d16d9cb9d0d7a025f21645c16b8 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/InlineTemplate.php @@ -0,0 +1,46 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\InlineTemplate. + */ + +namespace Drupal\Core\Render\Element; + +/** + * Provides a render element where the user supplies an in-line Twig template. + * + * @RenderElement("inline_template") + */ +class InlineTemplate extends RenderElement { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return array( + '#pre_render' => array( + array($class, 'preRenderInlineTemplate'), + ), + '#template' => '', + '#context' => array(), + ); + } + + /** + * Renders a twig string directly. + * + * @param array $element + * + * @return array + */ + public static function preRenderInlineTemplate($element) { + /** @var \Drupal\Core\Template\TwigEnvironment $environment */ + $environment = \Drupal::service('twig'); + $markup = $environment->renderInline($element['#template'], $element['#context']); + $element['#markup'] = $markup; + return $element; + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/Item.php b/core/lib/Drupal/Core/Render/Element/Item.php new file mode 100644 index 0000000000000000000000000000000000000000..145b9043eadd2fceb117cfea15c5ec5ef3a73515 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Item.php @@ -0,0 +1,37 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\Item. + */ + +namespace Drupal\Core\Render\Element; + +/** + * Provides a display-only form element with an optional title and description. + * + * Note: since this is a read-only field, setting the #required property will do + * nothing except theme the form element to look as if it were actually required + * (i.e. by placing a red star next to the #title). + * + * @FormElement("item") + */ +class Item extends FormElement { + + /** + * {@inheritdoc} + */ + public function getInfo() { + return array( + // Forms that show author fields to both anonymous and authenticated users + // need to dynamically switch between #type 'textfield' and #type 'item' + // to automatically take over the authenticated user's information. + // Therefore, we allow #type 'item' to receive input, which is internally + // assigned by Form API based on the #default_value or #value properties. + '#input' => TRUE, + '#markup' => '', + '#theme_wrappers' => array('form_element'), + ); + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/Label.php b/core/lib/Drupal/Core/Render/Element/Label.php new file mode 100644 index 0000000000000000000000000000000000000000..7f90605c0493fe0831d5bb46701e87068f146ace --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Label.php @@ -0,0 +1,29 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\Label. + */ + +namespace Drupal\Core\Render\Element; + +/** + * Provides a render element for displaying the label for a form element. + * + * Labels are generated automatically from element properties during processing + * of most form elements. + * + * @RenderElement("label") + */ +class Label extends RenderElement { + + /** + * {@inheritdoc} + */ + public function getInfo() { + return array( + '#theme' => 'form_element_label', + ); + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/LanguageSelect.php b/core/lib/Drupal/Core/Render/Element/LanguageSelect.php new file mode 100644 index 0000000000000000000000000000000000000000..433e50892d133a586ff5b94804c2f91608cf8769 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/LanguageSelect.php @@ -0,0 +1,29 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\LanguageSelect. + */ + +namespace Drupal\Core\Render\Element; + +use Drupal\Core\Language\LanguageInterface; + +/** + * Provides a form element for selecting a language. + * + * @FormElement("language_select") + */ +class LanguageSelect extends FormElement { + + /** + * {@inheritdoc} + */ + public function getInfo() { + return array( + '#input' => TRUE, + '#default_value' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + ); + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/Number.php b/core/lib/Drupal/Core/Render/Element/Number.php new file mode 100644 index 0000000000000000000000000000000000000000..5e395414384be48ffa26d49b4c084d6a96cf00c1 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Number.php @@ -0,0 +1,102 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\Number. + */ + +namespace Drupal\Core\Render\Element; + +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element; +use Drupal\Component\Utility\Number as NumberUtility; + +/** + * Provides a form element for numeric input, with special numeric validation. + * + * @FormElement("number") + */ +class Number extends FormElement { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return array( + '#input' => TRUE, + '#step' => 1, + '#process' => array( + array($class, 'processAjaxForm'), + ), + '#element_validate' => array( + array($class, 'validateNumber'), + ), + '#pre_render' => array( + array($class, 'preRenderNumber'), + ), + '#theme' => 'input__number', + '#theme_wrappers' => array('form_element'), + ); + } + + /** + * Form element validation handler for #type 'number'. + * + * Note that #required is validated by _form_validate() already. + */ + public static function validateNumber(&$element, FormStateInterface $form_state, &$complete_form) { + $value = $element['#value']; + if ($value === '') { + return; + } + + $name = empty($element['#title']) ? $element['#parents'][0] : $element['#title']; + + // Ensure the input is numeric. + if (!is_numeric($value)) { + $form_state->setError($element, t('%name must be a number.', array('%name' => $name))); + return; + } + + // Ensure that the input is greater than the #min property, if set. + if (isset($element['#min']) && $value < $element['#min']) { + $form_state->setError($element, t('%name must be higher than or equal to %min.', array('%name' => $name, '%min' => $element['#min']))); + } + + // Ensure that the input is less than the #max property, if set. + if (isset($element['#max']) && $value > $element['#max']) { + $form_state->setError($element, t('%name must be lower than or equal to %max.', array('%name' => $name, '%max' => $element['#max']))); + } + + if (isset($element['#step']) && strtolower($element['#step']) != 'any') { + // Check that the input is an allowed multiple of #step (offset by #min if + // #min is set). + $offset = isset($element['#min']) ? $element['#min'] : 0.0; + + if (!NumberUtility::validStep($value, $element['#step'], $offset)) { + $form_state->setError($element, t('%name is not a valid number.', array('%name' => $name))); + } + } + } + + /** + * Prepares a #type 'number' render element for theme_input(). + * + * @param array $element + * An associative array containing the properties of the element. + * Properties used: #title, #value, #description, #min, #max, #placeholder, + * #required, #attributes, #step, #size. + * + * @return array + * The $element with prepared variables ready for theme_input(). + */ + public static function preRenderNumber($element) { + $element['#attributes']['type'] = 'number'; + Element::setAttributes($element, array('id', 'name', 'value', 'step', 'min', 'max', 'placeholder', 'size')); + static::setAttributes($element, array('form-number')); + + return $element; + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/Operations.php b/core/lib/Drupal/Core/Render/Element/Operations.php new file mode 100644 index 0000000000000000000000000000000000000000..89bc68b729d7d631dc84f8313fc07441f633bd66 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Operations.php @@ -0,0 +1,30 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\Operations. + */ + +namespace Drupal\Core\Render\Element; + +/** + * Provides a render element for a set of operations links. + * + * This is a special case of \Drupal\Core\Render\Element\Dropbutton; the only + * difference is that it offers themes the possibility to render it differently + * through a theme suggestion. + * + * @RenderElement("operations") + */ +class Operations extends Dropbutton { + + /** + * {@inheritdoc} + */ + public function getInfo() { + return array( + '#theme' => 'links__dropbutton__operations', + ) + parent::getInfo(); + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/Page.php b/core/lib/Drupal/Core/Render/Element/Page.php new file mode 100644 index 0000000000000000000000000000000000000000..4156ab4e2d38a0ee92cf7e414433b4f17f6d393d --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Page.php @@ -0,0 +1,46 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\Page. + */ + +namespace Drupal\Core\Render\Element; + +/** + * Provides a render element for an entire HTML page. + * + * @RenderElement("page") + */ +class Page { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return array( + '#show_messages' => TRUE, + '#pre_render' => array( + array($class, 'preRenderPage'), + ), + '#theme' => 'page', + '#title' => '', + ); + } + + /** + * #pre_render callback for the page element type. + * + * @param array $element + * A structured array containing the page element type build properties. + * + * @return array + */ + public static function preRenderPage($element) { + $element['#cache']['tags']['theme'] = \Drupal::theme()->getActiveTheme()->getName(); + $element['#cache']['tags']['theme_global_settings'] = TRUE; + return $element; + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/Password.php b/core/lib/Drupal/Core/Render/Element/Password.php new file mode 100644 index 0000000000000000000000000000000000000000..48a20cf320e4a39faed4aad49ce3df09e187becb --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Password.php @@ -0,0 +1,59 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\Password. + */ + +namespace Drupal\Core\Render\Element; + +use Drupal\Core\Render\Element; + +/** + * Provides a form element for entering a password, with hidden text. + * + * @FormElement("password") + */ +class Password extends FormElement { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return array( + '#input' => TRUE, + '#size' => 60, + '#maxlength' => 128, + '#process' => array( + array($class, 'processAjaxForm'), + array($class, 'processPattern'), + ), + '#pre_render' => array( + array($class, 'preRenderPassword'), + ), + '#theme' => 'input__password', + '#theme_wrappers' => array('form_element'), + ); + } + + /** + * Prepares a #type 'password' render element for theme_input(). + * + * @param array $element + * An associative array containing the properties of the element. + * Properties used: #title, #value, #description, #size, #maxlength, + * #placeholder, #required, #attributes. + * + * @return array + * The $element with prepared variables ready for theme_input(). + */ + public static function preRenderPassword($element) { + $element['#attributes']['type'] = 'password'; + Element::setAttributes($element, array('id', 'name', 'size', 'maxlength', 'placeholder')); + static::setAttributes($element, array('form-text')); + + return $element; + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/PasswordConfirm.php b/core/lib/Drupal/Core/Render/Element/PasswordConfirm.php new file mode 100644 index 0000000000000000000000000000000000000000..f3ea4e835f504a2d7d2ab101f75f4e1469126509 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/PasswordConfirm.php @@ -0,0 +1,98 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\PasswordConfirm. + */ + +namespace Drupal\Core\Render\Element; + +use Drupal\Core\Form\FormStateInterface; + +/** + * Provides a form element for double-input of passwords. + * + * Formats as a pair of password fields, which do not validate unless the two + * entered passwords match. + * + * @FormElement("password_confirm") + */ +class PasswordConfirm extends FormElement { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return array( + '#input' => TRUE, + '#process' => array( + array($class, 'processPasswordConfirm'), + ), + '#theme_wrappers' => array('form_element'), + ); + } + + /** + * {@inheritdoc} + */ + public static function valueCallback(&$element, $input, FormStateInterface $form_state) { + if ($input === FALSE) { + $element += array('#default_value' => array()); + return $element['#default_value'] + array('pass1' => '', 'pass2' => ''); + } + } + + /** + * Expand a password_confirm field into two text boxes. + */ + public static function processPasswordConfirm(&$element, FormStateInterface $form_state, &$complete_form) { + $element['pass1'] = array( + '#type' => 'password', + '#title' => t('Password'), + '#value' => empty($element['#value']) ? NULL : $element['#value']['pass1'], + '#required' => $element['#required'], + '#attributes' => array('class' => array('password-field')), + ); + $element['pass2'] = array( + '#type' => 'password', + '#title' => t('Confirm password'), + '#value' => empty($element['#value']) ? NULL : $element['#value']['pass2'], + '#required' => $element['#required'], + '#attributes' => array('class' => array('password-confirm')), + ); + $element['#element_validate'] = array(array(get_called_class(), 'validatePasswordConfirm')); + $element['#tree'] = TRUE; + + if (isset($element['#size'])) { + $element['pass1']['#size'] = $element['pass2']['#size'] = $element['#size']; + } + + return $element; + } + + /** + * Validates a password_confirm element. + */ + public static function validatePasswordConfirm(&$element, FormStateInterface $form_state, &$complete_form) { + $pass1 = trim($element['pass1']['#value']); + $pass2 = trim($element['pass2']['#value']); + if (!empty($pass1) || !empty($pass2)) { + if (strcmp($pass1, $pass2)) { + $form_state->setError($element, t('The specified passwords do not match.')); + } + } + elseif ($element['#required'] && !empty($form_state['input'])) { + $form_state->setError($element, t('Password field is required.')); + } + + // Password field must be converted from a two-element array into a single + // string regardless of validation results. + $form_state->setValueForElement($element['pass1'], NULL); + $form_state->setValueForElement($element['pass2'], NULL); + $form_state->setValueForElement($element, $pass1); + + return $element; + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/Radio.php b/core/lib/Drupal/Core/Render/Element/Radio.php new file mode 100644 index 0000000000000000000000000000000000000000..f106d074b88941815b39dc8ee10afadb436b2f33 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Radio.php @@ -0,0 +1,68 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\Radio. + */ + +namespace Drupal\Core\Render\Element; + +use Drupal\Core\Render\Element; + +/** + * Provides a form element for a single radio button. + * + * @see \Drupal\Core\Render\Element\Radios + * + * @FormElement("radio") + */ +class Radio extends FormElement { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return array( + '#input' => TRUE, + '#default_value' => NULL, + '#process' => array( + array($class, 'processAjaxForm'), + ), + '#pre_render' => array( + array($class, 'preRenderRadio'), + ), + '#theme' => 'input__radio', + '#theme_wrappers' => array('form_element'), + '#title_display' => 'after', + ); + } + + /** + * Prepares a #type 'radio' render element for theme_input(). + * + * @param array $element + * An associative array containing the properties of the element. + * Properties used: #required, #return_value, #value, #attributes, #title, + * #description. + * + * Note: The input "name" attribute needs to be sanitized before output, which + * is currently done by initializing Drupal\Core\Template\Attribute with + * all the attributes. + * + * @return array + * The $element with prepared variables ready for theme_input(). + */ + public static function preRenderRadio($element) { + $element['#attributes']['type'] = 'radio'; + Element::setAttributes($element, array('id', 'name', '#return_value' => 'value')); + + if (isset($element['#return_value']) && $element['#value'] !== FALSE && $element['#value'] == $element['#return_value']) { + $element['#attributes']['checked'] = 'checked'; + } + static::setAttributes($element, array('form-radio')); + + return $element; + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/Radios.php b/core/lib/Drupal/Core/Render/Element/Radios.php new file mode 100644 index 0000000000000000000000000000000000000000..b0106285d8f3920d2217791b649ea941786df064 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Radios.php @@ -0,0 +1,106 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\Radios. + */ + +namespace Drupal\Core\Render\Element; + +use Drupal\Core\Form\FormStateInterface; + +/** + * Provides a form element for a set of radio buttons. + * + * @see \Drupal\Core\Render\Element\Checkboxes + * @see \Drupal\Core\Render\Element\Radio + * + * @FormElement("radios") + */ +class Radios extends FormElement { + + use CompositeFormElementTrait; + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return array( + '#input' => TRUE, + '#process' => array( + array($class, 'processRadios'), + ), + '#theme_wrappers' => array('radios'), + '#pre_render' => array( + array($class, 'preRenderCompositeFormElement'), + ), + ); + } + + /** + * Expands a radios element into individual radio elements. + */ + public static function processRadios(&$element, FormStateInterface $form_state, &$complete_form) { + if (count($element['#options']) > 0) { + $weight = 0; + foreach ($element['#options'] as $key => $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()); + // Generate the parents as the autogenerator does, so we will have a + // unique id for each radio button. + $parents_for_id = array_merge($element['#parents'], array($key)); + $element[$key] += array( + '#type' => 'radio', + '#title' => $choice, + // The key is sanitized in Drupal\Core\Template\Attribute during output + // from the theme function. + '#return_value' => $key, + // Use default or FALSE. A value of FALSE means that the radio button is + // not 'checked'. + '#default_value' => isset($element['#default_value']) ? $element['#default_value'] : FALSE, + '#attributes' => $element['#attributes'], + '#parents' => $element['#parents'], + '#id' => drupal_html_id('edit-' . implode('-', $parents_for_id)), + '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL, + '#weight' => $weight, + ); + } + } + return $element; + } + + /** + * {@inheritdoc} + */ + public static function valueCallback(&$element, $input, FormStateInterface $form_state) { + if ($input !== FALSE) { + // When there's user input (including NULL), return it as the value. + // However, if NULL is submitted, FormBuilder::handleInputElement() will + // apply the default value, and we want that validated against #options + // unless it's empty. (An empty #default_value, such as NULL or FALSE, can + // be used to indicate that no radio button is selected by default.) + if (!isset($input) && !empty($element['#default_value'])) { + $element['#needs_validation'] = TRUE; + } + return $input; + } + else { + // For default value handling, simply return #default_value. Additionally, + // for a NULL default value, set #has_garbage_value to prevent + // FormBuilder::handleInputElement() converting the NULL to an empty + // string, so that code can distinguish between nothing selected and the + // selection of a radio button whose value is an empty string. + $value = isset($element['#default_value']) ? $element['#default_value'] : NULL; + if (!isset($value)) { + $element['#has_garbage_value'] = TRUE; + } + return $value; + } + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/Range.php b/core/lib/Drupal/Core/Render/Element/Range.php new file mode 100644 index 0000000000000000000000000000000000000000..234d92fd29238fb11021b08bda28b768e6feeb6c --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Range.php @@ -0,0 +1,72 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\Range. + */ + +namespace Drupal\Core\Render\Element; + +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element; + +/** + * Provides a form element for input of a number within a specific range. + * + * @FormElement("range") + */ +class Range extends Number { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $info = parent::getInfo(); + $class = get_class($this); + return array( + '#min' => 0, + '#max' => 100, + '#pre_render' => array( + array($class, 'preRenderRange'), + ), + '#theme' => 'input__range', + ) + $info; + } + + /** + * Prepares a #type 'range' render element for theme_input(). + * + * @param array $element + * An associative array containing the properties of the element. + * Properties used: #title, #value, #description, #min, #max, #attributes, + * #step. + * + * @return array + * The $element with prepared variables ready for theme_input(). + */ + public static function preRenderRange($element) { + $element['#attributes']['type'] = 'range'; + Element::setAttributes($element, array('id', 'name', 'value', 'step', 'min', 'max')); + static::setAttributes($element, array('form-range')); + + return $element; + } + + /** + * {@inheritdoc} + */ + public static function valueCallback(&$element, $input, FormStateInterface $form_state) { + if ($input === '') { + $offset = ($element['#max'] - $element['#min']) / 2; + + // Round to the step. + if (strtolower($element['#step']) != 'any') { + $steps = round($offset / $element['#step']); + $offset = $element['#step'] * $steps; + } + + return $element['#min'] + $offset; + } + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/RenderElement.php b/core/lib/Drupal/Core/Render/Element/RenderElement.php index cf4d4546dd698b03a851670c907d18854973e7f4..3cbe18c3139903f7fc389abbb25636d5b414b3e1 100644 --- a/core/lib/Drupal/Core/Render/Element/RenderElement.php +++ b/core/lib/Drupal/Core/Render/Element/RenderElement.php @@ -7,11 +7,12 @@ namespace Drupal\Core\Render\Element; +use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\PluginBase; use Drupal\Core\Render\Element; /** - * Provides a base class for element render plugins. + * Provides a base class for render element plugins. * * @see \Drupal\Core\Render\Annotation\RenderElement * @see \Drupal\Core\Render\ElementInterface @@ -22,6 +23,30 @@ */ abstract class RenderElement extends PluginBase implements ElementInterface { + /** + * {@inheritdoc} + */ + public static function setAttributes(&$element, $class = array()) { + if (!empty($class)) { + if (!isset($element['#attributes']['class'])) { + $element['#attributes']['class'] = array(); + } + $element['#attributes']['class'] = array_merge($element['#attributes']['class'], $class); + } + // This function is invoked from form element theme functions, but the + // rendered form element may not necessarily have been processed by + // form_builder(). + if (!empty($element['#required'])) { + $element['#attributes']['class'][] = 'required'; + $element['#attributes']['required'] = 'required'; + $element['#attributes']['aria-required'] = 'true'; + } + if (isset($element['#parents']) && isset($element['#errors']) && !empty($element['#validated'])) { + $element['#attributes']['class'][] = 'error'; + $element['#attributes']['aria-invalid'] = 'true'; + } + } + /** * Adds members of this group as actual elements for rendering. * @@ -82,4 +107,66 @@ public static function preRenderGroup($element) { return $element; } + /** + * Form element processing handler for the #ajax form property. + * + * This method is useful for non-input elements that can be used in and + * outside the context of a form. + * + * @param array $element + * An associative array containing the properties of the 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 ajax_pre_render_element() + */ + public static function processAjaxForm(&$element, FormStateInterface $form_state, &$complete_form) { + $element = ajax_pre_render_element($element); + if (!empty($element['#ajax_processed'])) { + $form_state['cache'] = TRUE; + } + return $element; + } + + /** + * Arranges elements into groups. + * + * This method is useful for non-input elements that can be used in and + * outside the context of a form. + * + * @param array $element + * An associative array containing the properties and children of the + * element. Note that $element must be taken by reference here, so processed + * child elements are taken over into $form_state. + * @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. + */ + public static function processGroup(&$element, FormStateInterface $form_state, &$complete_form) { + $parents = implode('][', $element['#parents']); + + // Each details element forms a new group. The #type 'vertical_tabs' basically + // only injects a new details element. + $form_state['groups'][$parents]['#group_exists'] = TRUE; + $element['#groups'] = &$form_state['groups']; + + // Process vertical tabs group member details elements. + if (isset($element['#group'])) { + // Add this details element to the defined group (by reference). + $group = $element['#group']; + $form_state['groups'][$group][] = &$element; + } + + return $element; + } + } diff --git a/core/lib/Drupal/Core/Render/Element/Scripts.php b/core/lib/Drupal/Core/Render/Element/Scripts.php new file mode 100644 index 0000000000000000000000000000000000000000..cc04edec76cbebc431bf994407d017d5163fa205 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Scripts.php @@ -0,0 +1,67 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\Scripts. + */ + +namespace Drupal\Core\Render\Element; + +/** + * Provides a render element for adding JavaScript to the HTML output. + * + * @see \Drupal\Core\Render\Element\Styles + * + * @RenderElement("scripts") + */ +class Scripts extends RenderElement { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return array( + '#items' => array(), + '#pre_render' => array( + array($class, 'preRenderScripts'), + ), + ); + } + + /** + * #pre_render callback to add the elements needed for JavaScript tags to be rendered. + * + * This function evaluates the aggregation enabled/disabled condition on a group + * by group basis by testing whether an aggregate file has been made for the + * group rather than by testing the site-wide aggregation setting. This allows + * this function to work correctly even if modules have implemented custom + * logic for grouping and aggregating files. + * + * @param array $element + * A render array containing: + * - #items: The JavaScript items as returned by _drupal_add_js() and + * altered by drupal_get_js(). + * - #group_callback: A function to call to group #items. Following + * this function, #aggregate_callback is called to aggregate items within + * the same group into a single file. + * - #aggregate_callback: A function to call to aggregate the items within + * the groups arranged by the #group_callback function. + * + * @return array + * A render array that will render to a string of JavaScript tags. + * + * @see drupal_get_js() + */ + public static function preRenderScripts($element) { + $js_assets = $element['#items']; + + // Aggregate the JavaScript if necessary, but only during normal site + // operation. + if (!defined('MAINTENANCE_MODE') && \Drupal::config('system.performance')->get('js.preprocess')) { + $js_assets = \Drupal::service('asset.js.collection_optimizer')->optimize($js_assets); + } + return \Drupal::service('asset.js.collection_renderer')->render($js_assets); + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/Search.php b/core/lib/Drupal/Core/Render/Element/Search.php new file mode 100644 index 0000000000000000000000000000000000000000..dfa70ed42ed0764ca7fd75589f15514888a550aa --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Search.php @@ -0,0 +1,64 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\Search. + */ + +namespace Drupal\Core\Render\Element; + +use Drupal\Core\Render\Element; + +/** + * Provides a form input element for searching. + * + * This is commonly used to provide a filter or search box at the top of a + * long listing page, to allow users to find specific items in the list for + * faster input. + * + * @FormElement("search") + */ +class Search extends FormElement { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return array( + '#input' => TRUE, + '#size' => 60, + '#maxlength' => 128, + '#autocomplete_route_name' => FALSE, + '#process' => array( + array($class, 'processAutocomplete'), + array($class, 'processAjaxForm'), + ), + '#pre_render' => array( + array($class, 'preRenderSearch'), + ), + '#theme' => 'input__search', + '#theme_wrappers' => array('form_element'), + ); + } + + /** + * Prepares a #type 'search' render element for theme_input(). + * + * @param array $element + * An associative array containing the properties of the element. + * Properties used: #title, #value, #description, #size, #maxlength, + * #placeholder, #required, #attributes. + * + * @return array + * The $element with prepared variables ready for theme_input(). + */ + public static function preRenderSearch($element) { + $element['#attributes']['type'] = 'search'; + Element::setAttributes($element, array('id', 'name', 'value', 'size', 'maxlength', 'placeholder')); + static::setAttributes($element, array('form-search')); + + return $element; + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/Select.php b/core/lib/Drupal/Core/Render/Element/Select.php new file mode 100644 index 0000000000000000000000000000000000000000..f939641917db330c67178c2d2586165695d0284f --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Select.php @@ -0,0 +1,159 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\Select. + */ + +namespace Drupal\Core\Render\Element; + +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element; + +/** + * Provides a form element for a drop-down menu or scrolling selection box. + * + * See #empty_option and #empty_value for an explanation of various settings for + * a select element, including behavior if #required is TRUE or FALSE. + * + * @FormElement("select") + */ +class Select extends FormElement { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return array( + '#input' => TRUE, + '#multiple' => FALSE, + '#process' => array( + array($class, 'processSelect'), + array($class, 'processAjaxForm'), + ), + '#pre_render' => array( + array($class, 'preRenderSelect'), + ), + '#theme' => 'select', + '#theme_wrappers' => array('form_element'), + '#options' => array(), + ); + } + + /** + * Processes a select list form element. + * + * This process callback is mandatory for select fields, since all user agents + * automatically preselect the first available option of single (non-multiple) + * select lists. + * + * @param array $element + * The form element to process. Properties used: + * - #multiple: (optional) Indicates whether one or more options can be + * selected. Defaults to FALSE. + * - #default_value: Must be NULL or not set in case there is no value for the + * element yet, in which case a first default option is inserted by default. + * Whether this first option is a valid option depends on whether the field + * is #required or not. + * - #required: (optional) Whether the user needs to select an option (TRUE) + * or not (FALSE). Defaults to FALSE. + * - #empty_option: (optional) The label to show for the first default option. + * By default, the label is automatically set to "- Please select -" for a + * required field and "- None -" for an optional field. + * - #empty_value: (optional) The value for the first default option, which is + * used to determine whether the user submitted a value or not. + * - If #required is TRUE, this defaults to '' (an empty string). + * - If #required is not TRUE and this value isn't set, then no extra option + * is added to the select control, leaving the control in a slightly + * illogical state, because there's no way for the user to select nothing, + * since all user agents automatically preselect the first available + * option. But people are used to this being the behavior of select + * controls. + * @todo Address the above issue in Drupal 8. + * - If #required is not TRUE and this value is set (most commonly to an + * empty string), then an extra option (see #empty_option above) + * representing a "non-selection" is added with this as its value. + * @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 processSelect(&$element, FormStateInterface $form_state, &$complete_form) { + // #multiple select fields need a special #name. + if ($element['#multiple']) { + $element['#attributes']['multiple'] = 'multiple'; + $element['#attributes']['name'] = $element['#name'] . '[]'; + } + // A non-#multiple select needs special handling to prevent user agents from + // preselecting the first option without intention. #multiple select lists do + // not get an empty option, as it would not make sense, user interface-wise. + else { + // If the element is set to #required through #states, override the + // element's #required setting. + $required = isset($element['#states']['required']) ? TRUE : $element['#required']; + // If the element is required and there is no #default_value, then add an + // empty option that will fail validation, so that the user is required to + // make a choice. Also, if there's a value for #empty_value or + // #empty_option, then add an option that represents emptiness. + if (($required && !isset($element['#default_value'])) || isset($element['#empty_value']) || isset($element['#empty_option'])) { + $element += array( + '#empty_value' => '', + '#empty_option' => $required ? t('- Select -') : t('- None -'), + ); + // The empty option is prepended to #options and purposively not merged + // to prevent another option in #options mistakenly using the same value + // as #empty_value. + $empty_option = array($element['#empty_value'] => $element['#empty_option']); + $element['#options'] = $empty_option + $element['#options']; + } + } + return $element; + } + + /** + * {@inheritdoc} + */ + public static function valueCallback(&$element, $input, FormStateInterface $form_state) { + if ($input !== FALSE) { + if (isset($element['#multiple']) && $element['#multiple']) { + // If an enabled multi-select submits NULL, it means all items are + // unselected. A disabled multi-select always submits NULL, and the + // default value should be used. + if (empty($element['#disabled'])) { + return (is_array($input)) ? array_combine($input, $input) : array(); + } + else { + return (isset($element['#default_value']) && is_array($element['#default_value'])) ? $element['#default_value'] : array(); + } + } + // Non-multiple select elements may have an empty option preprended to them + // (see form_process_select()). When this occurs, usually #empty_value is + // an empty string, but some forms set #empty_value to integer 0 or some + // other non-string constant. PHP receives all submitted form input as + // strings, but if the empty option is selected, set the value to match the + // empty value exactly. + elseif (isset($element['#empty_value']) && $input === (string) $element['#empty_value']) { + return $element['#empty_value']; + } + else { + return $input; + } + } + } + + /** + * Prepares a select render element. + */ + public static function preRenderSelect($element) { + Element::setAttributes($element, array('id', 'name', 'size')); + static::setAttributes($element, array('form-select')); + return $element; + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/Styles.php b/core/lib/Drupal/Core/Render/Element/Styles.php new file mode 100644 index 0000000000000000000000000000000000000000..d285fac2243d5e94610c8e39f442fad4db65264c --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Styles.php @@ -0,0 +1,98 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\Styles. + */ + +namespace Drupal\Core\Render\Element; + +/** + * Provides a render element for adding CSS to the HTML output. + * + * @see \Drupal\Core\Render\Element\Scripts + * + * @RenderElement("styles") + */ +class Styles extends RenderElement { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return array( + '#items' => array(), + '#pre_render' => array( + array($class, 'preRenderStyles'), + ), + ); + } + + /** + * Pre-render callback: Adds the elements needed for CSS tags to be rendered. + * + * For production websites, LINK tags are preferable to STYLE tags with @import + * statements, because: + * - They are the standard tag intended for linking to a resource. + * - On Firefox 2 and perhaps other browsers, CSS files included with @import + * statements don't get saved when saving the complete web page for offline + * use: http://drupal.org/node/145218. + * - On IE, if only LINK tags and no @import statements are used, all the CSS + * files are downloaded in parallel, resulting in faster page load, but if + * @import statements are used and span across multiple STYLE tags, all the + * ones from one STYLE tag must be downloaded before downloading begins for + * the next STYLE tag. Furthermore, IE7 does not support media declaration on + * the @import statement, so multiple STYLE tags must be used when different + * files are for different media types. Non-IE browsers always download in + * parallel, so this is an IE-specific performance quirk: + * http://www.stevesouders.com/blog/2009/04/09/dont-use-import/. + * + * However, IE has an annoying limit of 31 total CSS inclusion tags + * (http://drupal.org/node/228818) and LINK tags are limited to one file per + * tag, whereas STYLE tags can contain multiple @import statements allowing + * multiple files to be loaded per tag. When CSS aggregation is disabled, a + * Drupal site can easily have more than 31 CSS files that need to be loaded, so + * using LINK tags exclusively would result in a site that would display + * incorrectly in IE. Depending on different needs, different strategies can be + * employed to decide when to use LINK tags and when to use STYLE tags. + * + * The strategy employed by this function is to use LINK tags for all aggregate + * files and for all files that cannot be aggregated (e.g., if 'preprocess' is + * set to FALSE or the type is 'external'), and to use STYLE tags for groups + * of files that could be aggregated together but aren't (e.g., if the site-wide + * aggregation setting is disabled). This results in all LINK tags when + * aggregation is enabled, a guarantee that as many or only slightly more tags + * are used with aggregation disabled than enabled (so that if the limit were to + * be crossed with aggregation enabled, the site developer would also notice the + * problem while aggregation is disabled), and an easy way for a developer to + * view HTML source while aggregation is disabled and know what files will be + * aggregated together when aggregation becomes enabled. + * + * This function evaluates the aggregation enabled/disabled condition on a group + * by group basis by testing whether an aggregate file has been made for the + * group rather than by testing the site-wide aggregation setting. This allows + * this function to work correctly even if modules have implemented custom + * logic for grouping and aggregating files. + * + * @param array $element + * A render array containing: + * - '#items': The CSS items as returned by _drupal_add_css() and altered by + * drupal_get_css(). + * + * @return array + * A render array that will render to a string of XHTML CSS tags. + * + * @see drupal_get_css() + */ + public static function preRenderStyles($element) { + $css_assets = $element['#items']; + + // Aggregate the CSS if necessary, but only during normal site operation. + if (!defined('MAINTENANCE_MODE') && \Drupal::config('system.performance')->get('css.preprocess')) { + $css_assets = \Drupal::service('asset.css.collection_optimizer')->optimize($css_assets); + } + return \Drupal::service('asset.css.collection_renderer')->render($css_assets); + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/Submit.php b/core/lib/Drupal/Core/Render/Element/Submit.php new file mode 100644 index 0000000000000000000000000000000000000000..41e592d3b48008d5caeab911eb2a73cef1ed82f9 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Submit.php @@ -0,0 +1,29 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\Submit. + */ + +namespace Drupal\Core\Render\Element; + +/** + * Provides a form submit button. + * + * Submit buttons are processed the same as regular buttons, except they trigger + * the form's submit handler. + * + * @FormElement("submit") + */ +class Submit extends Button { + + /** + * {@inheritdoc} + */ + public function getInfo() { + return array( + '#executes_submit_callback' => TRUE, + ) + parent::getInfo(); + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/Table.php b/core/lib/Drupal/Core/Render/Element/Table.php new file mode 100644 index 0000000000000000000000000000000000000000..191de689f9d503378e0b454694efb821376efbae --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Table.php @@ -0,0 +1,358 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\Table. + */ + +namespace Drupal\Core\Render\Element; + +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element; + +/** + * Provides a render element for a table. + * + * Note: Although this extends FormElement, it can be used outside the + * context of a form. + * + * @see \Drupal\Core\Render\Element\Tableselect + * + * @FormElement("table") + */ +class Table extends FormElement { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return array( + '#header' => array(), + '#rows' => array(), + '#empty' => '', + // Properties for tableselect support. + '#input' => TRUE, + '#tree' => TRUE, + '#tableselect' => FALSE, + '#sticky' => FALSE, + '#responsive' => TRUE, + '#multiple' => TRUE, + '#js_select' => TRUE, + '#process' => array( + array($class, 'processTable'), + ), + '#element_validate' => array( + array($class, 'validateTable'), + ), + // Properties for tabledrag support. + // The value is a list of arrays that are passed to + // drupal_attach_tabledrag(). drupal_pre_render_table() prepends the HTML + // ID of the table to each set of options. + // @see drupal_attach_tabledrag() + '#tabledrag' => array(), + // Render properties. + '#pre_render' => array( + array($class, 'preRenderTable'), + ), + '#theme' => 'table', + ); + } + + /** + * {@inheritdoc} + */ + public static function valueCallback(&$element, $input, FormStateInterface $form_state) { + // If #multiple is FALSE, the regular default value of radio buttons is used. + if (!empty($element['#tableselect']) && !empty($element['#multiple'])) { + // Contrary to #type 'checkboxes', the default value of checkboxes in a + // table is built from the array keys (instead of array values) of the + // #default_value property. + // @todo D8: Remove this inconsistency. + if ($input === FALSE) { + $element += array('#default_value' => array()); + $value = array_keys(array_filter($element['#default_value'])); + return array_combine($value, $value); + } + else { + return is_array($input) ? array_combine($input, $input) : array(); + } + } + } + + /** + * #process callback for #type 'table' to add tableselect support. + * + * @param array $element + * An associative array containing the properties and children of the + * table 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. + */ + public static function processTable(&$element, FormStateInterface $form_state, &$complete_form) { + if ($element['#tableselect']) { + if ($element['#multiple']) { + $value = is_array($element['#value']) ? $element['#value'] : array(); + } + // Advanced selection behavior makes no sense for radios. + else { + $element['#js_select'] = FALSE; + } + // Add a "Select all" checkbox column to the header. + // @todo D8: Rename into #select_all? + if ($element['#js_select']) { + $element['#attached']['library'][] = 'core/drupal.tableselect'; + array_unshift($element['#header'], array('class' => array('select-all'))); + } + // Add an empty header column for radio buttons or when a "Select all" + // checkbox is not desired. + else { + array_unshift($element['#header'], ''); + } + + if (!isset($element['#default_value']) || $element['#default_value'] === 0) { + $element['#default_value'] = array(); + } + // Create a checkbox or radio for each row in a way that the value of the + // tableselect element behaves as if it had been of #type checkboxes or + // radios. + foreach (Element::children($element) as $key) { + $row = &$element[$key]; + // Prepare the element #parents for the tableselect form element. + // Their values have to be located in child keys (#tree is ignored), since + // form_validate_table() has to be able to validate whether input (for the + // parent #type 'table' element) has been submitted. + $element_parents = array_merge($element['#parents'], array($key)); + + // Since the #parents of the tableselect form element will equal the + // #parents of the row element, prevent FormBuilder from auto-generating + // an #id for the row element, since drupal_html_id() would automatically + // append a suffix to the tableselect form element's #id otherwise. + $row['#id'] = drupal_html_id('edit-' . implode('-', $element_parents) . '-row'); + + // Do not overwrite manually created children. + if (!isset($row['select'])) { + // Determine option label; either an assumed 'title' column, or the + // first available column containing a #title or #markup. + // @todo Consider to add an optional $element[$key]['#title_key'] + // defaulting to 'title'? + unset($label_element); + $title = NULL; + if (isset($row['title']['#type']) && $row['title']['#type'] == 'label') { + $label_element = &$row['title']; + } + else { + if (!empty($row['title']['#title'])) { + $title = $row['title']['#title']; + } + else { + foreach (Element::children($row) as $column) { + if (isset($row[$column]['#title'])) { + $title = $row[$column]['#title']; + break; + } + if (isset($row[$column]['#markup'])) { + $title = $row[$column]['#markup']; + break; + } + } + } + if (isset($title) && $title !== '') { + $title = t('Update !title', array('!title' => $title)); + } + } + + // Prepend the select column to existing columns. + $row = array('select' => array()) + $row; + $row['select'] += array( + '#type' => $element['#multiple'] ? 'checkbox' : 'radio', + '#id' => drupal_html_id('edit-' . implode('-', $element_parents)), + // @todo If rows happen to use numeric indexes instead of string keys, + // this results in a first row with $key === 0, which is always FALSE. + '#return_value' => $key, + '#attributes' => $element['#attributes'], + '#wrapper_attributes' => array( + 'class' => array('table-select'), + ), + ); + if ($element['#multiple']) { + $row['select']['#default_value'] = isset($value[$key]) ? $key : NULL; + $row['select']['#parents'] = $element_parents; + } + else { + $row['select']['#default_value'] = ($element['#default_value'] == $key ? $key : NULL); + $row['select']['#parents'] = $element['#parents']; + } + if (isset($label_element)) { + $label_element['#id'] = $row['select']['#id'] . '--label'; + $label_element['#for'] = $row['select']['#id']; + $row['select']['#attributes']['aria-labelledby'] = $label_element['#id']; + $row['select']['#title_display'] = 'none'; + } + else { + $row['select']['#title'] = $title; + $row['select']['#title_display'] = 'invisible'; + } + } + } + } + + return $element; + } + + /** + * #element_validate callback for #type 'table'. + * + * @param array $element + * An associative array containing the properties and children of the + * table element. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * @param array $complete_form + * The complete form structure. + */ + public static function validateTable(&$element, FormStateInterface $form_state, &$complete_form) { + // Skip this validation if the button to submit the form does not require + // selected table row data. + if (empty($form_state['triggering_element']['#tableselect'])) { + return; + } + if ($element['#multiple']) { + if (!is_array($element['#value']) || !count(array_filter($element['#value']))) { + $form_state->setError($element, t('No items selected.')); + } + } + elseif (!isset($element['#value']) || $element['#value'] === '') { + $form_state->setError($element, t('No item selected.')); + } + } + + /** + * #pre_render callback to transform children of an element into #rows suitable for theme_table(). + * + * This function converts sub-elements of an element of #type 'table' to be + * suitable for theme_table(): + * - The first level of sub-elements are table rows. Only the #attributes + * property is taken into account. + * - The second level of sub-elements is converted into columns for the + * corresponding first-level table row. + * + * Simple example usage: + * @code + * $form['table'] = array( + * '#type' => 'table', + * '#header' => array(t('Title'), array('data' => t('Operations'), 'colspan' => '1')), + * // Optionally, to add tableDrag support: + * '#tabledrag' => array( + * array( + * 'action' => 'order', + * 'relationship' => 'sibling', + * 'group' => 'thing-weight', + * ), + * ), + * ); + * foreach ($things as $row => $thing) { + * $form['table'][$row]['#weight'] = $thing['weight']; + * + * $form['table'][$row]['title'] = array( + * '#type' => 'textfield', + * '#default_value' => $thing['title'], + * ); + * + * // Optionally, to add tableDrag support: + * $form['table'][$row]['#attributes']['class'][] = 'draggable'; + * $form['table'][$row]['weight'] = array( + * '#type' => 'textfield', + * '#title' => t('Weight for @title', array('@title' => $thing['title'])), + * '#title_display' => 'invisible', + * '#size' => 4, + * '#default_value' => $thing['weight'], + * '#attributes' => array('class' => array('thing-weight')), + * ); + * + * // The amount of link columns should be identical to the 'colspan' + * // attribute in #header above. + * $form['table'][$row]['edit'] = array( + * '#type' => 'link', + * '#title' => t('Edit'), + * '#href' => 'thing/' . $row . '/edit', + * ); + * } + * @endcode + * + * @param array $element + * A structured array containing two sub-levels of elements. Properties used: + * - #tabledrag: The value is a list of $options arrays that are passed to + * drupal_attach_tabledrag(). The HTML ID of the table is added to each + * $options array. + * + * @return array + * + * @see theme_table() + * @see drupal_process_attached() + * @see drupal_attach_tabledrag() + */ + public static function preRenderTable($element) { + foreach (Element::children($element) as $first) { + $row = array('data' => array()); + // Apply attributes of first-level elements as table row attributes. + if (isset($element[$first]['#attributes'])) { + $row += $element[$first]['#attributes']; + } + // Turn second-level elements into table row columns. + // @todo Do not render a cell for children of #type 'value'. + // @see http://drupal.org/node/1248940 + foreach (Element::children($element[$first]) as $second) { + // Assign the element by reference, so any potential changes to the + // original element are taken over. + $column = array('data' => &$element[$first][$second]); + + // Apply wrapper attributes of second-level elements as table cell + // attributes. + if (isset($element[$first][$second]['#wrapper_attributes'])) { + $column += $element[$first][$second]['#wrapper_attributes']; + } + + $row['data'][] = $column; + } + $element['#rows'][] = $row; + } + + // Take over $element['#id'] as HTML ID attribute, if not already set. + Element::setAttributes($element, array('id')); + + // Add sticky headers, if applicable. + if (count($element['#header']) && $element['#sticky']) { + $element['#attached']['library'][] = 'core/drupal.tableheader'; + // Add 'sticky-enabled' class to the table to identify it for JS. + // This is needed to target tables constructed by this function. + $element['#attributes']['class'][] = 'sticky-enabled'; + } + // If the table has headers and it should react responsively to columns hidden + // with the classes represented by the constants RESPONSIVE_PRIORITY_MEDIUM + // and RESPONSIVE_PRIORITY_LOW, add the tableresponsive behaviors. + if (count($element['#header']) && $element['#responsive']) { + $element['#attached']['library'][] = 'core/drupal.tableresponsive'; + // Add 'responsive-enabled' class to the table to identify it for JS. + // This is needed to target tables constructed by this function. + $element['#attributes']['class'][] = 'responsive-enabled'; + } + + // If the custom #tabledrag is set and there is a HTML ID, add the table's + // HTML ID to the options and attach the behavior. + if (!empty($element['#tabledrag']) && isset($element['#attributes']['id'])) { + foreach ($element['#tabledrag'] as $options) { + $options['table_id'] = $element['#attributes']['id']; + drupal_attach_tabledrag($element, $options); + } + } + + return $element; + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/Tableselect.php b/core/lib/Drupal/Core/Render/Element/Tableselect.php new file mode 100644 index 0000000000000000000000000000000000000000..08333692ef786b34780ef40ffa5af95500fff5f8 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Tableselect.php @@ -0,0 +1,255 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\Tableselect. + */ + +namespace Drupal\Core\Render\Element; + +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element; + +/** + * Provides a form element for a table with radios or checkboxes in left column. + * + * Build the table headings and columns with the #headers property, and the rows + * with the #options property. See https://www.drupal.org/node/94510 for a full + * explanation. + * + * @see \Drupal\Core\Render\Element\Table + * + * @FormElement("tableselect") + */ +class Tableselect extends Table { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return array( + '#input' => TRUE, + '#js_select' => TRUE, + '#multiple' => TRUE, + '#responsive' => TRUE, + '#sticky' => FALSE, + '#pre_render' => array( + array($class, 'preRenderTable'), + array($class, 'preRenderTableselect'), + ), + '#process' => array( + array($class, 'processTableselect'), + ), + '#options' => array(), + '#empty' => '', + '#theme' => 'table__tableselect', + ); + } + + /** + * {@inheritdoc} + */ + public static function valueCallback(&$element, $input, FormStateInterface $form_state) { + // If $element['#multiple'] == FALSE, then radio buttons are displayed and + // the default value handling is used. + if (isset($element['#multiple']) && $element['#multiple']) { + // Checkboxes are being displayed with the default value coming from the + // keys of the #default_value property. This differs from the checkboxes + // element which uses the array values. + if ($input === FALSE) { + $value = array(); + $element += array('#default_value' => array()); + foreach ($element['#default_value'] as $key => $flag) { + if ($flag) { + $value[$key] = $key; + } + } + return $value; + } + else { + return is_array($input) ? array_combine($input, $input) : array(); + } + } + } + + /** + * Prepares a 'tableselect' #type element for rendering. + * + * Adds a column of radio buttons or checkboxes for each row of a table. + * + * @param array $element + * An associative array containing the properties and children of + * the tableselect element. Properties used: #header, #options, #empty, + * and #js_select. The #options property is an array of selection options; + * each array element of #options is an array of properties. These + * properties can include #attributes, which is added to the + * table row's HTML attributes; see table.html.twig. An example of per-row + * options: + * @code + * $options = array( + * array( + * 'title' => 'How to Learn Drupal', + * 'content_type' => 'Article', + * 'status' => 'published', + * '#attributes' => array('class' => array('article-row')), + * ), + * array( + * 'title' => 'Privacy Policy', + * 'content_type' => 'Page', + * 'status' => 'published', + * '#attributes' => array('class' => array('page-row')), + * ), + * ); + * $header = array( + * 'title' => t('Title'), + * 'content_type' => t('Content type'), + * 'status' => t('Status'), + * ); + * $form['table'] = array( + * '#type' => 'tableselect', + * '#header' => $header, + * '#options' => $options, + * '#empty' => t('No content available.'), + * ); + * @endcode + * + * @return array + * The processed element. + */ + public static function preRenderTableselect($element) { + $rows = array(); + $header = $element['#header']; + if (!empty($element['#options'])) { + // Generate a table row for each selectable item in #options. + foreach (Element::children($element) as $key) { + $row = array(); + + $row['data'] = array(); + if (isset($element['#options'][$key]['#attributes'])) { + $row += $element['#options'][$key]['#attributes']; + } + // Render the checkbox / radio element. + $row['data'][] = drupal_render($element[$key]); + + // As theme_table only maps header and row columns by order, create the + // correct order by iterating over the header fields. + foreach ($element['#header'] as $fieldname => $title) { + // A row cell can span over multiple headers, which means less row cells + // than headers could be present. + if (isset($element['#options'][$key][$fieldname])) { + // A header can span over multiple cells and in this case the cells + // are passed in an array. The order of this array determines the + // order in which they are added. + if (is_array($element['#options'][$key][$fieldname]) && !isset($element['#options'][$key][$fieldname]['data'])) { + foreach ($element['#options'][$key][$fieldname] as $cell) { + $row['data'][] = $cell; + } + } + else { + $row['data'][] = $element['#options'][$key][$fieldname]; + } + } + } + $rows[] = $row; + } + // Add an empty header or a "Select all" checkbox to provide room for the + // checkboxes/radios in the first table column. + if ($element['#js_select']) { + // Add a "Select all" checkbox. + $element['#attached']['library'][] = 'core/drupal.tableselect'; + array_unshift($header, array('class' => array('select-all'))); + } + else { + // Add an empty header when radio buttons are displayed or a "Select all" + // checkbox is not desired. + array_unshift($header, ''); + } + } + + $element['#header'] = $header; + $element['#rows'] = $rows; + + return $element; + } + + /** + * Creates checkbox or radio elements to populate a tableselect table. + * + * @param array $element + * An associative array containing the properties and children of the + * tableselect 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. + */ + public static function processTableselect(&$element, FormStateInterface $form_state, &$complete_form) { + if ($element['#multiple']) { + $value = is_array($element['#value']) ? $element['#value'] : array(); + } + else { + // Advanced selection behavior makes no sense for radios. + $element['#js_select'] = FALSE; + } + + $element['#tree'] = TRUE; + + if (count($element['#options']) > 0) { + if (!isset($element['#default_value']) || $element['#default_value'] === 0) { + $element['#default_value'] = array(); + } + + // Create a checkbox or radio for each item in #options in such a way that + // the value of the tableselect element behaves as if it had been of type + // checkboxes or radios. + foreach ($element['#options'] as $key => $choice) { + // Do not overwrite manually created children. + if (!isset($element[$key])) { + if ($element['#multiple']) { + $title = ''; + if (!empty($element['#options'][$key]['title']['data']['#title'])) { + $title = t('Update @title', array( + '@title' => $element['#options'][$key]['title']['data']['#title'], + )); + } + $element[$key] = array( + '#type' => 'checkbox', + '#title' => $title, + '#title_display' => 'invisible', + '#return_value' => $key, + '#default_value' => isset($value[$key]) ? $key : NULL, + '#attributes' => $element['#attributes'], + ); + } + else { + // Generate the parents as the autogenerator does, so we will have a + // unique id for each radio button. + $parents_for_id = array_merge($element['#parents'], array($key)); + $element[$key] = array( + '#type' => 'radio', + '#title' => '', + '#return_value' => $key, + '#default_value' => ($element['#default_value'] == $key) ? $key : NULL, + '#attributes' => $element['#attributes'], + '#parents' => $element['#parents'], + '#id' => drupal_html_id('edit-' . implode('-', $parents_for_id)), + '#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL, + ); + } + if (isset($element['#options'][$key]['#weight'])) { + $element[$key]['#weight'] = $element['#options'][$key]['#weight']; + } + } + } + } + else { + $element['#value'] = array(); + } + return $element; + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/Tel.php b/core/lib/Drupal/Core/Render/Element/Tel.php new file mode 100644 index 0000000000000000000000000000000000000000..5cd4c5dd1f926a89155227c6b4d793daa8671b0f --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Tel.php @@ -0,0 +1,61 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\Tel. + */ + +namespace Drupal\Core\Render\Element; + +use Drupal\Core\Render\Element; + +/** + * Provides a form element for entering a telephone number. + * + * @FormElement("tel") + */ +class Tel extends FormElement { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return array( + '#input' => TRUE, + '#size' => 30, + '#maxlength' => 128, + '#autocomplete_route_name' => FALSE, + '#process' => array( + array($class, 'processAutocomplete'), + array($class, 'processAjaxForm'), + array($class, 'processPattern'), + ), + '#pre_render' => array( + array($class, 'preRenderTel'), + ), + '#theme' => 'input__tel', + '#theme_wrappers' => array('form_element'), + ); + } + + /** + * Prepares a #type 'tel' render element for theme_input(). + * + * @param array $element + * An associative array containing the properties of the element. + * Properties used: #title, #value, #description, #size, #maxlength, + * #placeholder, #required, #attributes. + * + * @return array + * The $element with prepared variables ready for theme_input(). + */ + public static function preRenderTel($element) { + $element['#attributes']['type'] = 'tel'; + Element::setAttributes($element, array('id', 'name', 'value', 'size', 'maxlength', 'placeholder')); + static::setAttributes($element, array('form-tel')); + + return $element; + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/Textarea.php b/core/lib/Drupal/Core/Render/Element/Textarea.php new file mode 100644 index 0000000000000000000000000000000000000000..bb5fad7bca25483f7d3aaea2480c6d8f5ceb3953 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Textarea.php @@ -0,0 +1,41 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\Textarea. + */ + +namespace Drupal\Core\Render\Element; + +use Drupal\Core\Render\Element; + +/** + * Provides a form element for input of multiple-line text. + * + * @FormElement("textarea") + */ +class Textarea extends FormElement { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return array( + '#input' => TRUE, + '#cols' => 60, + '#rows' => 5, + '#resizable' => 'vertical', + '#process' => array( + array($class, 'processAjaxForm'), + array($class, 'processGroup'), + ), + '#pre_render' => array( + array($class, 'preRenderGroup'), + ), + '#theme' => 'textarea', + '#theme_wrappers' => array('form_element'), + ); + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/Textfield.php b/core/lib/Drupal/Core/Render/Element/Textfield.php index d687a2cf8c2680f0fbeb237cad9d9344fbd39dfd..43964948bb5ebab269bfa1a9810b2c33e27aa333 100644 --- a/core/lib/Drupal/Core/Render/Element/Textfield.php +++ b/core/lib/Drupal/Core/Render/Element/Textfield.php @@ -67,7 +67,7 @@ public static function valueCallback(&$element, $input, FormStateInterface $form public static function preRenderTextfield($element) { $element['#attributes']['type'] = 'text'; Element::setAttributes($element, array('id', 'name', 'value', 'size', 'maxlength', 'placeholder')); - _form_set_attributes($element, array('form-text')); + static::setAttributes($element, array('form-text')); return $element; } diff --git a/core/lib/Drupal/Core/Render/Element/Token.php b/core/lib/Drupal/Core/Render/Element/Token.php new file mode 100644 index 0000000000000000000000000000000000000000..094660f9a3b2652abca53be5f9a001646b9c8d17 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Token.php @@ -0,0 +1,46 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\Token. + */ + +namespace Drupal\Core\Render\Element; + +use Drupal\Core\Form\FormStateInterface; + +/** + * Stores token data in a hidden form field. + * + * This is generally used to protect against cross-site forgeries. A token + * element is automatically added to each Drupal form by drupal_prepare_form(), + * so you don't generally have to add one yourself. + * + * @FormElement("token") + */ +class Token extends Hidden { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return array( + '#input' => TRUE, + '#pre_render' => array( + array($class, 'preRenderHidden'), + ), + '#theme' => 'input__hidden', + ); + } + + /** + * {@inheritdoc} + */ + public static function valueCallback(&$element, $input, FormStateInterface $form_state) { + if ($input !== FALSE) { + return (string) $input; + } + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/Url.php b/core/lib/Drupal/Core/Render/Element/Url.php new file mode 100644 index 0000000000000000000000000000000000000000..e8911280c6c5554b3833d7cfb16313b9d6ff96fc --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Url.php @@ -0,0 +1,80 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\Url. + */ + +namespace Drupal\Core\Render\Element; + +use Drupal\Component\Utility\UrlHelper; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element; + +/** + * Provides a form element for input of a URL. + * + * @FormElement("url") + */ +class Url extends FormElement { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return array( + '#input' => TRUE, + '#size' => 60, + '#maxlength' => 255, + '#autocomplete_route_name' => FALSE, + '#process' => array( + array($class, 'processAutocomplete'), + array($class, 'processAjaxForm'), + array($class, 'processPattern'), + ), + '#element_validate' => array( + array($class, 'validateUrl'), + ), + '#pre_render' => array( + array($class, 'preRenderUrl'), + ), + '#theme' => 'input__url', + '#theme_wrappers' => array('form_element'), + ); + } + + /** + * Form element validation handler for #type 'url'. + * + * Note that #maxlength and #required is validated by _form_validate() already. + */ + public static function validateUrl(&$element, FormStateInterface $form_state, &$complete_form) { + $value = trim($element['#value']); + $form_state->setValueForElement($element, $value); + + if ($value !== '' && !UrlHelper::isValid($value, TRUE)) { + $form_state->setError($element, t('The URL %url is not valid.', array('%url' => $value))); + } + } + + /** + * Prepares a #type 'url' render element for theme_input(). + * + * @param array $element + * An associative array containing the properties of the element. + * Properties used: #title, #value, #description, #size, #maxlength, + * #placeholder, #required, #attributes. + * + * @return array + * The $element with prepared variables ready for theme_input(). + */ + public static function preRenderUrl($element) { + $element['#attributes']['type'] = 'url'; + Element::setAttributes($element, array('id', 'name', 'value', 'size', 'maxlength', 'placeholder')); + static::setAttributes($element, array('form-url')); + + return $element; + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/Value.php b/core/lib/Drupal/Core/Render/Element/Value.php new file mode 100644 index 0000000000000000000000000000000000000000..5da3e0c9bd9fb7c8d4846bd19369020ec29a96b7 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Value.php @@ -0,0 +1,30 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\Value. + */ + +namespace Drupal\Core\Render\Element; + +/** + * Provides a form element for storage of internal information. + * + * Unlike \Drupal\Core\Render\Element\Hidden, this information is not + * sent to the browser in a hidden form field, but is just stored in the form + * array for use in validation and submit processing. + * + * @FormElement("value") + */ +class Value extends FormElement { + + /** + * {@inheritdoc} + */ + public function getInfo() { + return array( + '#input' => TRUE, + ); + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/VerticalTabs.php b/core/lib/Drupal/Core/Render/Element/VerticalTabs.php new file mode 100644 index 0000000000000000000000000000000000000000..336c52b49a026eb316aee9005900da0cd9d2f7b4 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/VerticalTabs.php @@ -0,0 +1,107 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\VerticalTabs. + */ + +namespace Drupal\Core\Render\Element; + +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element; + +/** + * Provides a render element for vertical tabs in a form. + * + * Formats all child fieldsets and all non-child fieldsets whose #group is + * assigned this element's name as vertical tabs. + * + * @FormElement("vertical_tabs") + */ +class VerticalTabs extends RenderElement { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return array( + '#default_tab' => '', + '#process' => array( + array($class, 'processVerticalTabs'), + ), + '#pre_render' => array( + array($class, 'preRenderVerticalTabs'), + ), + '#theme_wrappers' => array('vertical_tabs', 'form_element'), + ); + } + + /** + * Prepares a vertical_tabs element for rendering. + * + * @param array $element + * An associative array containing the properties and children of the + * vertical tabs element. + * + * @return array + * The modified element. + */ + public static function preRenderVerticalTabs($element) { + // Do not render the vertical tabs element if it is empty. + $group = implode('][', $element['#parents']); + if (!Element::getVisibleChildren($element['group']['#groups'][$group])) { + $element['#printed'] = TRUE; + } + return $element; + } + + /** + * Creates a group formatted as vertical tabs. + * + * @param array $element + * An associative array containing the properties and children of the + * details 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. + */ + public static function processVerticalTabs(&$element, FormStateInterface $form_state, &$complete_form) { + // Inject a new details as child, so that form_process_details() processes + // this details element like any other details. + $element['group'] = array( + '#type' => 'details', + '#theme_wrappers' => array(), + '#parents' => $element['#parents'], + ); + + // Add an invisible label for accessibility. + if (!isset($element['#title'])) { + $element['#title'] = t('Vertical Tabs'); + $element['#title_display'] = 'invisible'; + } + + $element['#attached']['library'][] = 'core/drupal.vertical-tabs'; + + // The JavaScript stores the currently selected tab in this hidden + // field so that the active tab can be restored the next time the + // form is rendered, e.g. on preview pages or when form validation + // fails. + $name = implode('__', $element['#parents']); + if ($form_state->hasValue($name . '__active_tab')){ + $element['#default_tab'] = $form_state->getValue($name . '__active_tab'); + } + $element[$name . '__active_tab'] = array( + '#type' => 'hidden', + '#default_value' => $element['#default_tab'], + '#attributes' => array('class' => array('vertical-tabs-active-tab')), + ); + + return $element; + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/Weight.php b/core/lib/Drupal/Core/Render/Element/Weight.php new file mode 100644 index 0000000000000000000000000000000000000000..ffcda6b381def2ba11f73bac8c7283fa257497ee --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Weight.php @@ -0,0 +1,67 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\Weight. + */ + +namespace Drupal\Core\Render\Element; + +use Drupal\Core\Form\FormStateInterface; + +/** + * Provides a form element for input of a weight. + * + * Weights are integers used to indicate ordering, with larger numbers later + * in the order. + * + * @FormElement("weight") + */ +class Weight extends FormElement { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return array( + '#input' => TRUE, + '#delta' => 10, + '#default_value' => 0, + '#process' => array( + array($class, 'processWeight'), + array($class, 'processAjaxForm'), + ), + ); + } + + /** + * Expands a weight element into a select element. + */ + public static function processWeight(&$element, FormStateInterface $form_state, &$complete_form) { + $element['#is_weight'] = TRUE; + + $element_info_manager = \Drupal::service('element_info'); + // If the number of options is small enough, use a select field. + $max_elements = \Drupal::config('system.site')->get('weight_select_max'); + if ($element['#delta'] <= $max_elements) { + $element['#type'] = 'select'; + $weights = array(); + for ($n = (-1 * $element['#delta']); $n <= $element['#delta']; $n++) { + $weights[$n] = $n; + } + $element['#options'] = $weights; + $element += $element_info_manager->getInfo('select'); + } + // Otherwise, use a text field. + else { + $element['#type'] = 'number'; + // Use a field big enough to fit most weights. + $element['#size'] = 10; + $element += $element_info_manager->getInfo('number'); + } + + return $element; + } + +} diff --git a/core/modules/color/color.module b/core/modules/color/color.module index 44a2a7acdf113ec069838589a00d0ff43e2e08ee..7010ddfb3d3eca6c4eae410c9bead20edc85799c 100644 --- a/core/modules/color/color.module +++ b/core/modules/color/color.module @@ -10,6 +10,7 @@ use Drupal\Component\Utility\SafeMarkup; use Drupal\Component\Utility\String; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element\Textfield; use Drupal\Core\Routing\RouteMatchInterface; /** @@ -301,7 +302,7 @@ function color_palette_color_value($element, $input = FALSE, FormStateInterface if ($input !== FALSE) { // Start with the provided value for this textfield, and validate that if // necessary, falling back on the default value. - $value = form_type_textfield_value($element, $input, $form_state); + $value = Textfield::valueCallback($element, $input, $form_state); if (!$value || !isset($form_state['complete form']['#token']) || color_valid_hexadecimal_string($value) || \Drupal::csrfToken()->validate($form_state->getValue('form_token'), $form_state['complete form']['#token'])) { return $value; } diff --git a/core/modules/comment/src/Tests/CommentStringIdEntitiesTest.php b/core/modules/comment/src/Tests/CommentStringIdEntitiesTest.php index a34adb4c211fa80b1f2d174b31f1770eb3420e39..de2111231c9dc67932a05ed51e00040d94b51722 100644 --- a/core/modules/comment/src/Tests/CommentStringIdEntitiesTest.php +++ b/core/modules/comment/src/Tests/CommentStringIdEntitiesTest.php @@ -29,8 +29,6 @@ class CommentStringIdEntitiesTest extends KernelTestBase { 'field_ui', 'entity', 'entity_test', - // EMAIL_MAX_LENGTH constant. - 'system', 'text', ); diff --git a/core/modules/datetime/datetime.module b/core/modules/datetime/datetime.module index d0114e280ab097ce9d56e12697edbe85c52df514..99cf2fd576a1469219c109f45bca9296779da73e 100644 --- a/core/modules/datetime/datetime.module +++ b/core/modules/datetime/datetime.module @@ -66,8 +66,13 @@ function datetime_element_info() { $types['datetime'] = array( '#input' => TRUE, '#element_validate' => array('datetime_datetime_validate'), - '#process' => array('datetime_datetime_form_process', 'form_process_group'), - '#pre_render' => array('form_pre_render_group'), + '#process' => array( + 'datetime_datetime_form_process', + array('Drupal\Core\Render\Element\RenderElement', 'processGroup'), + ), + '#pre_render' => array( + array('Drupal\Core\Render\Element\RenderElement', 'preRenderGroup'), + ), '#theme' => 'datetime_form', '#theme_wrappers' => array('datetime_wrapper'), '#date_date_format' => $date_format, diff --git a/core/modules/language/language.module b/core/modules/language/language.module index cbec3d5027e98a2ffd8560869ebdc4db6f134392..e095d8d072084716913017ca1522e3dd34db93c0 100644 --- a/core/modules/language/language.module +++ b/core/modules/language/language.module @@ -135,7 +135,11 @@ function language_element_info_alter(&$type) { if (!isset($type['language_select']['#theme_wrappers'])) { $type['language_select']['#theme_wrappers'] = array(); } - $type['language_select']['#process'] = array_merge($type['language_select']['#process'], array('language_process_language_select', 'form_process_select', 'ajax_process_form')); + $type['language_select']['#process'] = array_merge($type['language_select']['#process'], array( + 'language_process_language_select', + array('Drupal\Core\Render\Element\Select', 'processSelect'), + array('Drupal\Core\Render\Element\RenderElement', 'processAjaxForm'), + )); $type['language_select']['#theme'] = 'select'; $type['language_select']['#theme_wrappers'] = array_merge($type['language_select']['#theme_wrappers'], array('form_element')); $type['language_select']['#languages'] = LanguageInterface::STATE_CONFIGURABLE; diff --git a/core/modules/system/src/Tests/Common/JavaScriptTest.php b/core/modules/system/src/Tests/Common/JavaScriptTest.php index 76ab88dcbf20ce1fffa06de00f7c9c37dea22b1c..6fc3c56376d808a68407d2e7a5e2a84988b6a6aa 100644 --- a/core/modules/system/src/Tests/Common/JavaScriptTest.php +++ b/core/modules/system/src/Tests/Common/JavaScriptTest.php @@ -341,7 +341,7 @@ function testDifferentWeight() { /** * Tests adding JavaScript within conditional comments. * - * @see drupal_pre_render_conditional_comments() + * @see \Drupal\Core\Render\Element\HtmlTag::preRenderConditionalComments() */ function testBrowserConditionalComments() { $default_query_string = $this->container->get('state')->get('system.css_js_query_string') ?: '0'; diff --git a/core/modules/system/system.module b/core/modules/system/system.module index dc372f8d6ee6eff75824f439889934582089bfd9..4c6bc602e23742e2f8ea049ac2c21a9be5d2c821 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -64,18 +64,6 @@ */ const REGIONS_ALL = 'all'; -/** - * Defines the max length for an email address - * - * The maximum length of an email address is 254 characters. RFC 3696 - * specifies a total length of 320 characters, but mentions that - * addresses longer than 256 characters are not normally useful. Erratum - * 1690 was then released which corrected this value to 254 characters. - * @see http://tools.ietf.org/html/rfc3696#section-3 - * @see http://www.rfc-editor.org/errata_search.php?rfc=3696&eid=1690 - */ -const EMAIL_MAX_LENGTH = 254; - /** * Implements hook_help(). */ @@ -284,360 +272,6 @@ function system_hook_info() { return $hooks; } -/** - * Implements hook_element_info(). - */ -function system_element_info() { - // Top level elements. - $types['html'] = array( - '#theme' => 'html', - '#pre_render' => array('drupal_pre_render_html'), - // HTML5 Shiv - '#attached' => array( - 'library' => array('core/html5shiv'), - ), - ); - $types['form'] = array( - '#method' => 'post', - '#action' => request_uri(), - '#theme_wrappers' => array('form'), - ); - $types['page'] = array( - '#show_messages' => TRUE, - '#pre_render' => array('drupal_pre_render_page'), - '#theme' => 'page', - '#title' => '', - ); - $types['inline_template'] = array( - '#pre_render' => array('drupal_render_inline_template'), - '#template' => '', - '#context' => array(), - ); - // By default, we don't want Ajax commands being rendered in the context of an - // HTML page, so we don't provide defaults for #theme or #theme_wrappers. - // However, modules can set these properties (for example, to provide an HTML - // debugging page that displays rather than executes Ajax commands). - $types['ajax'] = array( - '#header' => TRUE, - '#commands' => array(), - '#error' => NULL, - ); - $types['html_tag'] = array( - '#pre_render' => array('drupal_pre_render_conditional_comments', 'drupal_pre_render_html_tag'), - '#attributes' => array(), - '#value' => NULL, - ); - $types['styles'] = array( - '#items' => array(), - '#pre_render' => array('drupal_pre_render_styles'), - ); - $types['scripts'] = array( - '#items' => array(), - '#pre_render' => array('drupal_pre_render_scripts'), - ); - - // Input elements. - $types['submit'] = array( - '#input' => TRUE, - '#name' => 'op', - '#is_button' => TRUE, - '#executes_submit_callback' => TRUE, - '#limit_validation_errors' => FALSE, - '#process' => array('form_process_button', 'ajax_process_form'), - '#pre_render' => array('form_pre_render_button'), - '#theme_wrappers' => array('input__submit'), - ); - $types['button'] = array( - '#input' => TRUE, - '#name' => 'op', - '#is_button' => TRUE, - '#executes_submit_callback' => FALSE, - '#limit_validation_errors' => FALSE, - '#process' => array('form_process_button', 'ajax_process_form'), - '#pre_render' => array('form_pre_render_button'), - '#theme_wrappers' => array('input__button'), - ); - $types['image_button'] = array( - '#input' => TRUE, - '#is_button' => TRUE, - '#executes_submit_callback' => TRUE, - '#limit_validation_errors' => FALSE, - '#process' => array('form_process_button', 'ajax_process_form'), - '#return_value' => TRUE, - '#has_garbage_value' => TRUE, - '#src' => NULL, - '#pre_render' => array('form_pre_render_image_button'), - '#theme_wrappers' => array('input__image_button'), - ); - $types['tel'] = array( - '#input' => TRUE, - '#size' => 30, - '#maxlength' => 128, - '#autocomplete_route_name' => FALSE, - '#process' => array('form_process_autocomplete', 'ajax_process_form', 'form_process_pattern'), - '#pre_render' => array('form_pre_render_tel'), - '#theme' => 'input__tel', - '#theme_wrappers' => array('form_element'), - ); - $types['email'] = array( - '#input' => TRUE, - '#size' => 60, - '#maxlength' => EMAIL_MAX_LENGTH, - '#autocomplete_route_name' => FALSE, - '#process' => array('form_process_autocomplete', 'ajax_process_form', 'form_process_pattern'), - '#element_validate' => array('form_validate_email'), - '#pre_render' => array('form_pre_render_email'), - '#theme' => 'input__email', - '#theme_wrappers' => array('form_element'), - ); - $types['url'] = array( - '#input' => TRUE, - '#size' => 60, - '#maxlength' => 255, - '#autocomplete_route_name' => FALSE, - '#process' => array('form_process_autocomplete', 'ajax_process_form', 'form_process_pattern'), - '#element_validate' => array('form_validate_url'), - '#pre_render' => array('form_pre_render_url'), - '#theme' => 'input__url', - '#theme_wrappers' => array('form_element'), - ); - $types['search'] = array( - '#input' => TRUE, - '#size' => 60, - '#maxlength' => 128, - '#autocomplete_route_name' => FALSE, - '#process' => array('form_process_autocomplete', 'ajax_process_form'), - '#pre_render' => array('form_pre_render_search'), - '#theme' => 'input__search', - '#theme_wrappers' => array('form_element'), - ); - $types['number'] = array( - '#input' => TRUE, - '#step' => 1, - '#process' => array('ajax_process_form'), - '#element_validate' => array('form_validate_number'), - '#pre_render' => array('form_pre_render_number'), - '#theme' => 'input__number', - '#theme_wrappers' => array('form_element'), - ); - $types['range'] = array( - '#input' => TRUE, - '#step' => 1, - '#min' => 0, - '#max' => 100, - '#process' => array('ajax_process_form'), - '#element_validate' => array('form_validate_number'), - '#pre_render' => array('form_pre_render_range'), - '#theme' => 'input__range', - '#theme_wrappers' => array('form_element'), - ); - $types['color'] = array( - '#input' => TRUE, - '#process' => array('ajax_process_form'), - '#element_validate' => array('form_validate_color'), - '#pre_render' => array('form_pre_render_color'), - '#theme' => 'input__color', - '#theme_wrappers' => array('form_element'), - ); - $types['password'] = array( - '#input' => TRUE, - '#size' => 60, - '#maxlength' => 128, - '#process' => array('ajax_process_form', 'form_process_pattern'), - '#pre_render' => array('form_pre_render_password'), - '#theme' => 'input__password', - '#theme_wrappers' => array('form_element'), - ); - $types['password_confirm'] = array( - '#input' => TRUE, - '#process' => array('form_process_password_confirm', 'user_form_process_password_confirm'), - '#theme_wrappers' => array('form_element'), - ); - $types['textarea'] = array( - '#input' => TRUE, - '#cols' => 60, - '#rows' => 5, - '#resizable' => 'vertical', - '#process' => array('ajax_process_form', 'form_process_group'), - '#pre_render' => array('form_pre_render_group'), - '#theme' => 'textarea', - '#theme_wrappers' => array('form_element'), - ); - $types['radios'] = array( - '#input' => TRUE, - '#process' => array('form_process_radios'), - '#theme_wrappers' => array('radios'), - '#pre_render' => array('form_pre_render_conditional_form_element'), - ); - $types['radio'] = array( - '#input' => TRUE, - '#default_value' => NULL, - '#process' => array('ajax_process_form'), - '#pre_render' => array('form_pre_render_radio'), - '#theme' => 'input__radio', - '#theme_wrappers' => array('form_element'), - '#title_display' => 'after', - ); - $types['checkboxes'] = array( - '#input' => TRUE, - '#process' => array('form_process_checkboxes'), - '#pre_render' => array('form_pre_render_conditional_form_element'), - '#theme_wrappers' => array('checkboxes'), - ); - $types['checkbox'] = array( - '#input' => TRUE, - '#return_value' => 1, - '#process' => array('form_process_checkbox', 'ajax_process_form', 'form_process_group'), - '#pre_render' => array('form_pre_render_checkbox', 'form_pre_render_group'), - '#theme' => 'input__checkbox', - '#theme_wrappers' => array('form_element'), - '#title_display' => 'after', - ); - $types['select'] = array( - '#input' => TRUE, - '#multiple' => FALSE, - '#process' => array('form_process_select', 'ajax_process_form'), - '#theme' => 'select', - '#theme_wrappers' => array('form_element'), - '#options' => array(), - ); - $types['language_select'] = array( - '#input' => TRUE, - '#default_value' => LanguageInterface::LANGCODE_NOT_SPECIFIED, - ); - $types['weight'] = array( - '#input' => TRUE, - '#delta' => 10, - '#default_value' => 0, - '#process' => array('form_process_weight', 'ajax_process_form'), - ); - $types['date'] = array( - '#input' => TRUE, - '#theme' => 'input__date', - '#pre_render' => array('form_pre_render_date'), - '#theme_wrappers' => array('form_element'), - ); - $types['file'] = array( - '#input' => TRUE, - '#multiple' => FALSE, - '#process' => array('form_process_file'), - '#size' => 60, - '#pre_render' => array('form_pre_render_file'), - '#theme' => 'input__file', - '#theme_wrappers' => array('form_element'), - ); - $types['tableselect'] = array( - '#input' => TRUE, - '#js_select' => TRUE, - '#multiple' => TRUE, - '#responsive' => TRUE, - '#sticky' => FALSE, - '#pre_render' => array('drupal_pre_render_table', 'form_pre_render_tableselect'), - '#process' => array('form_process_tableselect'), - '#options' => array(), - '#empty' => '', - '#theme' => 'table__tableselect', - ); - - // Form structure. - $types['label'] = array( - '#theme' => 'form_element_label', - ); - $types['item'] = array( - // Forms that show author fields to both anonymous and authenticated users - // need to dynamically switch between #type 'textfield' and #type 'item' to - // automatically take over the authenticated user's information. Therefore, - // we allow #type 'item' to receive input, which is internally assigned by - // Form API based on the #default_value or #value properties. - '#input' => TRUE, - '#markup' => '', - '#theme_wrappers' => array('form_element'), - ); - $types['hidden'] = array( - '#input' => TRUE, - '#process' => array('ajax_process_form'), - '#pre_render' => array('form_pre_render_hidden'), - '#theme' => 'input__hidden', - ); - $types['token'] = array( - '#input' => TRUE, - '#pre_render' => array('form_pre_render_hidden'), - '#theme' => 'input__hidden', - ); - $types['value'] = array( - '#input' => TRUE, - ); - $types['fieldset'] = array( - '#value' => NULL, - '#process' => array('form_process_group', 'ajax_process_form'), - '#pre_render' => array('form_pre_render_group'), - '#theme_wrappers' => array('fieldset'), - ); - $types['fieldgroup'] = $types['fieldset'] + array( - '#attributes' => array('class' => array('fieldgroup')), - ); - $types['details'] = array( - '#open' => FALSE, - '#value' => NULL, - '#process' => array('form_process_group', 'ajax_process_form'), - '#pre_render' => array('form_pre_render_details', 'form_pre_render_group'), - '#theme_wrappers' => array('details'), - ); - $types['vertical_tabs'] = array( - '#default_tab' => '', - '#process' => array('form_process_vertical_tabs'), - '#pre_render' => array('form_pre_render_vertical_tabs'), - '#theme_wrappers' => array('vertical_tabs', 'form_element'), - ); - $types['dropbutton'] = array( - '#pre_render' => array('drupal_pre_render_dropbutton'), - '#theme' => 'links__dropbutton', - ); - $types['operations'] = array( - '#pre_render' => array('drupal_pre_render_dropbutton'), - '#theme' => 'links__dropbutton__operations', - ); - - $types['container'] = array( - '#process' => array('form_process_group', 'form_process_container'), - '#pre_render' => array('form_pre_render_group'), - '#theme_wrappers' => array('container'), - ); - $types['actions'] = array( - '#process' => array('form_pre_render_actions_dropbutton', 'form_process_actions', 'form_process_container'), - '#weight' => 100, - '#theme_wrappers' => array('container'), - ); - - $types['table'] = array( - '#header' => array(), - '#rows' => array(), - '#empty' => '', - // Properties for tableselect support. - '#input' => TRUE, - '#tree' => TRUE, - '#tableselect' => FALSE, - '#sticky' => FALSE, - '#responsive' => TRUE, - '#multiple' => TRUE, - '#js_select' => TRUE, - '#value_callback' => 'form_type_table_value', - '#process' => array('form_process_table'), - '#element_validate' => array('form_validate_table'), - // Properties for tabledrag support. - // The value is a list of arrays that are passed to - // drupal_attach_tabledrag(). drupal_pre_render_table() prepends the HTML ID - // of the table to each set of options. - // @see drupal_attach_tabledrag() - '#tabledrag' => array(), - // Render properties. - '#pre_render' => array('drupal_pre_render_table'), - '#theme' => 'table', - ); - - return $types; -} - /** * Implements hook_theme_suggestions_HOOK(). */ diff --git a/core/modules/system/templates/form-element.html.twig b/core/modules/system/templates/form-element.html.twig index e029b18901c26f49b5f5aff5c10714238da1b922..4c359ba4fc3567fd4d3a5d520f5718fc14e5644d 100644 --- a/core/modules/system/templates/form-element.html.twig +++ b/core/modules/system/templates/form-element.html.twig @@ -22,10 +22,10 @@ * property hides the label for everyone except screen readers. * - attribute: Set the title attribute on the element to create a tooltip but * output no label element. This is supported only for checkboxes and radios - * in form_pre_render_conditional_form_element(). It is used where a visual - * label is not needed, such as a table of checkboxes where the row and - * column provide the context. The tooltip will include the title and - * required marker. + * in \Drupal\Core\Render\Element\CompositeFormElementTrait::preRenderCompositeFormElement(). + * It is used where a visual label is not needed, such as a table of + * checkboxes where the row and column provide the context. The tooltip will + * include the title and required marker. * - description: (optional) A list of description properties containing: * - content: A description of the form element, may not be set. * - attributes: (optional) A list of HTML attributes to apply to the diff --git a/core/modules/user/src/Form/UserPasswordForm.php b/core/modules/user/src/Form/UserPasswordForm.php index c2ed909f02e6817b3852320b33f643ffb14d6b21..378f8f3a1854b4067d1b50857cb0df585c1a1ef1 100644 --- a/core/modules/user/src/Form/UserPasswordForm.php +++ b/core/modules/user/src/Form/UserPasswordForm.php @@ -11,6 +11,7 @@ use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\Core\Render\Element\Email; use Drupal\user\UserStorageInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -75,7 +76,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { '#type' => 'textfield', '#title' => $this->t('Username or email address'), '#size' => 60, - '#maxlength' => max(USERNAME_MAX_LENGTH, EMAIL_MAX_LENGTH), + '#maxlength' => max(USERNAME_MAX_LENGTH, Email::EMAIL_MAX_LENGTH), '#required' => TRUE, '#attributes' => array( 'autocorrect' => 'off', diff --git a/core/modules/user/src/Tests/UserValidationTest.php b/core/modules/user/src/Tests/UserValidationTest.php index 97ee0e30b256b8556ab51baa69283116d022e0da..36da033430b18c4b5542e4255d021ee0d4365882 100644 --- a/core/modules/user/src/Tests/UserValidationTest.php +++ b/core/modules/user/src/Tests/UserValidationTest.php @@ -9,6 +9,7 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Field\Plugin\Field\FieldType\EmailItem; +use Drupal\Core\Render\Element\Email; use Drupal\simpletest\DrupalUnitTestBase; /** @@ -101,7 +102,7 @@ function testValidation() { $this->assertEqual($violations[0]->getPropertyPath(), 'mail.0.value'); $this->assertEqual($violations[0]->getMessage(), t('This value is not a valid email address.')); - $mail = $this->randomMachineName(EMAIL_MAX_LENGTH - 11) . '@example.com'; + $mail = $this->randomMachineName(Email::EMAIL_MAX_LENGTH - 11) . '@example.com'; $user->set('mail', $mail); $violations = $user->validate(); // @todo There are two violations because EmailItem::getConstraints() @@ -110,7 +111,7 @@ function testValidation() { // https://drupal.org/node/2023465. $this->assertEqual(count($violations), 2, 'Violations found when email is too long'); $this->assertEqual($violations[0]->getPropertyPath(), 'mail.0.value'); - $this->assertEqual($violations[0]->getMessage(), t('%name: the email address can not be longer than @max characters.', array('%name' => $user->get('mail')->getFieldDefinition()->getLabel(), '@max' => EMAIL_MAX_LENGTH))); + $this->assertEqual($violations[0]->getMessage(), t('%name: the email address can not be longer than @max characters.', array('%name' => $user->get('mail')->getFieldDefinition()->getLabel(), '@max' => Email::EMAIL_MAX_LENGTH))); $this->assertEqual($violations[1]->getPropertyPath(), 'mail.0.value'); $this->assertEqual($violations[1]->getMessage(), t('This value is not a valid email address.')); diff --git a/core/modules/user/user.module b/core/modules/user/user.module index f2b1984a962a38822d5a98a9e686fcd4748217e7..75777bbe671e2aa0b3f0c7db454ee2e4d41bb278 100644 --- a/core/modules/user/user.module +++ b/core/modules/user/user.module @@ -1363,14 +1363,21 @@ function _user_mail_notify($op, $account, $langcode = NULL) { return empty($mail) ? NULL : $mail['result']; } +/** + * Implements hook_element_info_alter(). + */ +function user_element_info_alter(array &$types) { + if (isset($types['password_confirm'])) { + $types['password_confirm']['#process'][] = 'user_form_process_password_confirm'; + } +} + /** * Form element process handler for client-side password validation. * * This #process handler is automatically invoked for 'password_confirm' form * elements to add the JavaScript and string translations for dynamic password * validation. - * - * @see system_element_info() */ function user_form_process_password_confirm($element) { $password_settings = array( diff --git a/core/modules/views_ui/views_ui.theme.inc b/core/modules/views_ui/views_ui.theme.inc index eb8c34f1a6ebf624fb13e2dde0990a8b4d5febc0..4164bd731bc9bc7edc719b88b54adb7fb7d682d7 100644 --- a/core/modules/views_ui/views_ui.theme.inc +++ b/core/modules/views_ui/views_ui.theme.inc @@ -6,6 +6,7 @@ */ use Drupal\Component\Utility\SafeMarkup; +use Drupal\Core\Form\FormState; use Drupal\Core\Render\Element; use Drupal\Core\Template\Attribute; @@ -141,8 +142,9 @@ function theme_views_ui_build_group_filter_form($variables) { t('Operations'), ); - $form['default_group'] = form_process_radios($form['default_group']); - $form['default_group_multiple'] = form_process_checkboxes($form['default_group_multiple']); + $form_state = new FormState(); + $form['default_group'] = Element\Radios::processRadios($form['default_group'], $form_state, $form); + $form['default_group_multiple'] = Element\Checkboxes::processCheckboxes($form['default_group_multiple'], $form_state, $form); $form['default_group']['All']['#title'] = ''; hide($form['default_group_multiple']['All']);