diff --git a/core/core.services.yml b/core/core.services.yml index 200972e73b32797b03efbfdc3d96c1a0e7667082..2f04124df99dd0c96e6d1105b1397609649ec834 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -749,6 +749,9 @@ services: plugin.manager.condition: class: Drupal\Core\Condition\ConditionManager parent: default_plugin_manager + plugin.manager.element_info: + class: Drupal\Core\Render\ElementInfoManager + parent: default_plugin_manager kernel_destruct_subscriber: class: Drupal\Core\EventSubscriber\KernelDestructionSubscriber tags: @@ -907,8 +910,7 @@ services: info_parser: class: Drupal\Core\Extension\InfoParser element_info: - class: Drupal\Core\Render\ElementInfo - arguments: ['@module_handler'] + alias: plugin.manager.element_info file.mime_type.guesser: class: Drupal\Core\File\MimeType\MimeTypeGuesser tags: diff --git a/core/includes/ajax.inc b/core/includes/ajax.inc index 4a03c6869b68b2a02d82da87cfda5fb093948fe7..3b5c2779b133e696bbfd193444c23e79efc7ed86 100644 --- a/core/includes/ajax.inc +++ b/core/includes/ajax.inc @@ -6,6 +6,7 @@ */ use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element; /** * @defgroup ajax Ajax API @@ -162,20 +163,10 @@ /** * Form element processing handler for the #ajax form property. * - * @param $element - * An associative array containing the properties of the element. - * - * @return - * The processed element. - * - * @see ajax_pre_render_element() + * @deprecated Use \Drupal\Core\Render\Element\FormElement::processAjaxForm(). */ -function ajax_process_form($element, FormStateInterface $form_state) { - $element = ajax_pre_render_element($element); - if (!empty($element['#ajax_processed'])) { - $form_state['cache'] = TRUE; - } - return $element; +function ajax_process_form($element, FormStateInterface $form_state, &$complete_form) { + return Element\FormElement::processAjaxForm($element, $form_state, $complete_form); } /** diff --git a/core/includes/common.inc b/core/includes/common.inc index 6c6c4a3a88af884027d6b5c41b3f6bef22103872..18ed1d510460cdd4479ea426cce8ed053c8435c3 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -2670,65 +2670,10 @@ function drupal_pre_render_html_tag($element) { /** * Pre-render callback: Renders a link into #markup. * - * Doing so during pre_render gives modules a chance to alter the link parts. - * - * @param $elements - * A structured array whose keys form the arguments to l(): - * - #title: The link text to pass as argument to l(). - * - One of the following - * - #route_name and (optionally) and a #route_parameters array; The route - * name and route parameters which will be passed into the link generator. - * - #href: The system path or URL to pass as argument to l(). - * - #options: (optional) An array of options to pass to l() or the link - * generator. - * - * @return - * The passed-in elements containing a rendered link in '#markup'. + * @deprecated Use \Drupal\Core\Render\Element\Link::preRenderLink(). */ function drupal_pre_render_link($element) { - // By default, link options to pass to l() are normally set in #options. - $element += array('#options' => array()); - // However, within the scope of renderable elements, #attributes is a valid - // way to specify attributes, too. Take them into account, but do not override - // attributes from #options. - if (isset($element['#attributes'])) { - $element['#options'] += array('attributes' => array()); - $element['#options']['attributes'] += $element['#attributes']; - } - - // This #pre_render callback can be invoked from inside or outside of a Form - // API context, and depending on that, a HTML ID may be already set in - // different locations. #options should have precedence over Form API's #id. - // #attributes have been taken over into #options above already. - if (isset($element['#options']['attributes']['id'])) { - $element['#id'] = $element['#options']['attributes']['id']; - } - elseif (isset($element['#id'])) { - $element['#options']['attributes']['id'] = $element['#id']; - } - - // Conditionally invoke ajax_pre_render_element(), if #ajax is set. - if (isset($element['#ajax']) && !isset($element['#ajax_processed'])) { - // If no HTML ID was found above, automatically create one. - if (!isset($element['#id'])) { - $element['#id'] = $element['#options']['attributes']['id'] = drupal_html_id('ajax-link'); - } - // If #ajax['path] was not specified, use the href as Ajax request URL. - if (!isset($element['#ajax']['path'])) { - $element['#ajax']['path'] = $element['#href']; - $element['#ajax']['options'] = $element['#options']; - } - $element = ajax_pre_render_element($element); - } - - if (isset($element['#route_name'])) { - $element['#route_parameters'] = empty($element['#route_parameters']) ? array() : $element['#route_parameters']; - $element['#markup'] = \Drupal::linkGenerator()->generate($element['#title'], $element['#route_name'], $element['#route_parameters'], $element['#options']); - } - else { - $element['#markup'] = l($element['#title'], $element['#href'], $element['#options']); - } - return $element; + return Element\Link::preRenderLink($element); } /** diff --git a/core/includes/form.inc b/core/includes/form.inc index f49dbd26fd16f8c51771df73fa96cea5346586d8..fb9add490ad613a63b0c3c06a4792fa5da5f4a93 100644 --- a/core/includes/form.inc +++ b/core/includes/form.inc @@ -566,22 +566,10 @@ function form_type_select_value($element, $input = FALSE) { /** * Determines the value for a textfield 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. + * @deprecated Use \Drupal\Core\Render\Element\Textfield::valueCallback(). */ -function form_type_textfield_value($element, $input = FALSE) { - if ($input !== FALSE && $input !== NULL) { - // Equate $input to the form value to ensure it's marked for - // validation. - return str_replace(array("\r", "\n"), '', $input); - } +function form_type_textfield_value(&$element, $input, &$form_state) { + return Element\Textfield::valueCallback($element, $input, $form_state); } /** @@ -1306,24 +1294,10 @@ function form_pre_render_actions_dropbutton(array $element) { /** * #process callback for #pattern form element property. * - * @param $element - * An associative array containing the properties and children of the - * generic input element. - * @param $form_state - * The current state of the form for the form this element belongs to. - * - * @return - * The processed element. - * - * @see form_validate_pattern() + * @deprecated Use \Drupal\Core\Render\Element\FormElement::processPattern(). */ -function form_process_pattern($element, FormStateInterface $form_state) { - if (isset($element['#pattern']) && !isset($element['#attributes']['pattern'])) { - $element['#attributes']['pattern'] = $element['#pattern']; - $element['#element_validate'][] = 'form_validate_pattern'; - } - - return $element; +function form_process_pattern($element, FormStateInterface $form_state, &$complete_form) { + return Element\FormElement::processPattern($element, $form_state, $complete_form); } /** @@ -1922,61 +1896,10 @@ function form_pre_render_details($element) { /** * Adds members of this group as actual elements for rendering. * - * @param $element - * An associative array containing the properties and children of the - * element. - * - * @return - * The modified element with all group members. + * @deprecated Use \Drupal\Core\Render\ElementElementBase::preRenderGroup(). */ function form_pre_render_group($element) { - // The element may be rendered outside of a Form API context. - if (!isset($element['#parents']) || !isset($element['#groups'])) { - return $element; - } - - // Inject group member elements belonging to this group. - $parents = implode('][', $element['#parents']); - $children = Element::children($element['#groups'][$parents]); - if (!empty($children)) { - foreach ($children as $key) { - // Break references and indicate that the element should be rendered as - // group member. - $child = (array) $element['#groups'][$parents][$key]; - $child['#group_details'] = TRUE; - // Inject the element as new child element. - $element[] = $child; - - $sort = TRUE; - } - // Re-sort the element's children if we injected group member elements. - if (isset($sort)) { - $element['#sorted'] = FALSE; - } - } - - if (isset($element['#group'])) { - // Contains form element summary functionalities. - $element['#attached']['library'][] = 'core/drupal.form'; - - $group = $element['#group']; - // If this element belongs to a group, but the group-holding element does - // not exist, we need to render it (at its original location). - if (!isset($element['#groups'][$group]['#group_exists'])) { - // Intentionally empty to clarify the flow; we simply return $element. - } - // If we injected this element into the group, then we want to render it. - elseif (!empty($element['#group_details'])) { - // Intentionally empty to clarify the flow; we simply return $element. - } - // Otherwise, this element belongs to a group and the group exists, so we do - // not render it. - elseif (Element::children($element['#groups'][$group])) { - $element['#printed'] = TRUE; - } - } - - return $element; + return Element\RenderElement::preRenderGroup($element); } /** @@ -2064,43 +1987,10 @@ function template_preprocess_vertical_tabs(&$variables) { * Adds autocomplete functionality to elements with a valid * #autocomplete_route_name. * - * Suppose your autocomplete route name is 'mymodule.autocomplete' and its path - * is: '/mymodule/autocomplete/{a}/{b}' - * In your form you have: - * @code - * '#autocomplete_route_name' => 'mymodule.autocomplete', - * '#autocomplete_route_parameters' => array('a' => $some_key, 'b' => $some_id), - * @endcode - * The user types in "keywords" so the full path called is: - * 'mymodule_autocomplete/$some_key/$some_id?q=keywords' - * - * @param array $element - * The form element to process. Properties used: - * - #autocomplete_route_name: A route to be used as callback URL by the - * autocomplete JavaScript library. - * - #autocomplete_route_parameters: The parameters to be used in conjunction - * with the route name. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - * - * @return array - * The form element. + * @deprecated Use \Drupal\Core\Render\Element\FormElement::processAutocomplete(). */ -function form_process_autocomplete($element, FormStateInterface $form_state) { - $access = FALSE; - if (!empty($element['#autocomplete_route_name'])) { - $parameters = isset($element['#autocomplete_route_parameters']) ? $element['#autocomplete_route_parameters'] : array(); - - $path = \Drupal::urlGenerator()->generate($element['#autocomplete_route_name'], $parameters); - $access = \Drupal::service('access_manager')->checkNamedRoute($element['#autocomplete_route_name'], $parameters, \Drupal::currentUser()); - } - if ($access) { - $element['#attributes']['class'][] = 'form-autocomplete'; - $element['#attached']['library'][] = 'core/drupal.autocomplete'; - // Provide a data attribute for the JavaScript behavior to bind to. - $element['#attributes']['data-autocomplete-path'] = $path; - } - return $element; +function form_process_autocomplete($element, FormStateInterface $form_state, &$complete_form) { + return Element\FormElement::processAutocomplete($element, $form_state, $complete_form); } /** @@ -2206,20 +2096,10 @@ function form_pre_render_hidden($element) { /** * Prepares a #type 'textfield' 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(). + * @deprecated Use \Drupal\Core\Render\Element\Textfield::preRenderTextfield(). */ function form_pre_render_textfield($element) { - $element['#attributes']['type'] = 'text'; - Element::setAttributes($element, array('id', 'name', 'value', 'size', 'maxlength', 'placeholder')); - _form_set_attributes($element, array('form-text')); - - return $element; + return Element\Textfield::preRenderTextfield($element); } /** diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php index adbb8f335681932d49c7fd704521f8adbe27dac5..d4170a412818c91f73b3d2e0535e0654e6c79659 100644 --- a/core/lib/Drupal/Core/Form/FormBuilder.php +++ b/core/lib/Drupal/Core/Form/FormBuilder.php @@ -938,7 +938,14 @@ protected function handleInputElement($form_id, &$element, FormStateInterface &$ // Set the element's #value property. if (!isset($element['#value']) && !array_key_exists('#value', $element)) { + // @todo Once all elements are converted to plugins in + // https://www.drupal.org/node/2311393, rely on + // $element['#value_callback'] directly. $value_callable = !empty($element['#value_callback']) ? $element['#value_callback'] : 'form_type_' . $element['#type'] . '_value'; + if (!is_callable($value_callable)) { + $value_callable = '\Drupal\Core\Render\Element\FormElement::valueCallback'; + } + if ($process_input) { // Get the input for the current element. NULL values in the input need // to be explicitly distinguished from missing input. (see below) @@ -962,9 +969,8 @@ protected function handleInputElement($form_id, &$element, FormStateInterface &$ // If we have input for the current element, assign it to the #value // property, optionally filtered through $value_callback. if ($input_exists) { - if (is_callable($value_callable)) { - $element['#value'] = call_user_func_array($value_callable, array(&$element, $input, &$form_state)); - } + $element['#value'] = call_user_func_array($value_callable, array(&$element, $input, &$form_state)); + if (!isset($element['#value']) && isset($input)) { $element['#value'] = $input; } @@ -978,9 +984,8 @@ protected function handleInputElement($form_id, &$element, FormStateInterface &$ if (!isset($element['#value'])) { // Call #type_value without a second argument to request default_value // handling. - if (is_callable($value_callable)) { - $element['#value'] = call_user_func_array($value_callable, array(&$element, FALSE, &$form_state)); - } + $element['#value'] = call_user_func_array($value_callable, array(&$element, FALSE, &$form_state)); + // Final catch. If we haven't set a value yet, use the explicit default // value. Avoid image buttons (which come with garbage value), so we // only get value for the button actually clicked. diff --git a/core/lib/Drupal/Core/Render/Annotation/FormElement.php b/core/lib/Drupal/Core/Render/Annotation/FormElement.php new file mode 100644 index 0000000000000000000000000000000000000000..4d4b59339c7d1590f93ba82c0beadbb2c29e79ee --- /dev/null +++ b/core/lib/Drupal/Core/Render/Annotation/FormElement.php @@ -0,0 +1,32 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Annotation\FormElement. + */ + +namespace Drupal\Core\Render\Annotation; + +/** + * Defines a form element plugin annotation object. + * + * See \Drupal\Core\Render\Element\FormElementInterface for more information + * about form element plugins. + * + * Plugin Namespace: Element + * + * For a working example, see \Drupal\Core\Render\Element\Textfield. + * + * @see \Drupal\Core\Render\ElementInfoManager + * @see \Drupal\Core\Render\Element\FormElementInterface + * @see \Drupal\Core\Render\Element\FormElement + * @see \Drupal\Core\Render\Annotation\RenderElement + * @see plugin_api + * + * @ingroup theme_render + * + * @Annotation + */ +class FormElement extends RenderElement { + +} diff --git a/core/lib/Drupal/Core/Render/Annotation/RenderElement.php b/core/lib/Drupal/Core/Render/Annotation/RenderElement.php new file mode 100644 index 0000000000000000000000000000000000000000..fc36b3234d0016d744e2ab196f015f8e77a271ed --- /dev/null +++ b/core/lib/Drupal/Core/Render/Annotation/RenderElement.php @@ -0,0 +1,34 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Annotation\RenderElement. + */ + +namespace Drupal\Core\Render\Annotation; + +use Drupal\Component\Annotation\PluginID; + +/** + * Defines a render element plugin annotation object. + * + * See \Drupal\Core\Render\Element\ElementInterface for more information + * about render element plugins. + * + * Plugin Namespace: Element + * + * For a working example, see \Drupal\Core\Render\Element\Link. + * + * @see \Drupal\Core\Render\ElementInfoManager + * @see \Drupal\Core\Render\Element\ElementInterface + * @see \Drupal\Core\Render\Element\RenderElement + * @see \Drupal\Core\Render\Annotation\FormElement + * @see plugin_api + * + * @ingroup theme_render + * + * @Annotation + */ +class RenderElement extends PluginID { + +} diff --git a/core/lib/Drupal/Core/Render/Element.php b/core/lib/Drupal/Core/Render/Element.php index 501f2de43ba0fcf1bd1387b4befee85a003f11b4..eed5c30fde840d5c06ced499f4cb0d28076382bc 100644 --- a/core/lib/Drupal/Core/Render/Element.php +++ b/core/lib/Drupal/Core/Render/Element.php @@ -10,7 +10,11 @@ use Drupal\Component\Utility\String; /** - * Deals with drupal render elements. + * Provides helper methods for Drupal render elements. + * + * @see \Drupal\Core\Render\Element\ElementInterface + * + * @ingroup theme_render */ class Element { @@ -47,7 +51,7 @@ public static function properties(array $element) { * The key to check. * * @return bool - * TRUE if the element is a child, FALSE otherwise. + * TRUE if the element is a child, FALSE otherwise. */ public static function child($key) { return !isset($key[0]) || $key[0] != '#'; @@ -143,11 +147,11 @@ public static function getVisibleChildren(array $elements) { * @param array $element * The renderable element to process. Passed by reference. * @param array $map - * An associative array whose keys are element property names and whose values - * are the HTML attribute names to set for corresponding the property; e.g., - * array('#propertyname' => 'attributename'). If both names are identical - * except for the leading '#', then an attribute name value is sufficient and - * no property name needs to be specified. + * An associative array whose keys are element property names and whose + * values are the HTML attribute names to set on the corresponding + * property; e.g., array('#propertyname' => 'attributename'). If both names + * are identical except for the leading '#', then an attribute name value is + * sufficient and no property name needs to be specified. */ public static function setAttributes(array &$element, array $map) { foreach ($map as $property => $attribute) { diff --git a/core/lib/Drupal/Core/Render/Element/ElementInterface.php b/core/lib/Drupal/Core/Render/Element/ElementInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..a1035f7b2b76a8b25ea4c9a3f684bfe6b93c23c2 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/ElementInterface.php @@ -0,0 +1,45 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\ElementInterface. + */ + +namespace Drupal\Core\Render\Element; + +use Drupal\Component\Plugin\PluginInspectionInterface; + +/** + * Provides an interface for 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 + * by the getInfo() method of the element plugin will be merged with the + * properties specified in render arrays. Thus, you can specify defaults for any + * Render API keys, in addition to those explicitly documented by + * \Drupal\Core\Render\ElementInfoManagerInterface::getInfo(). + * + * Some render elements are specifically form input elements; see + * \Drupal\Core\Render\Element\FormElementInterface for more information. + * + * @see \Drupal\Core\Render\ElementInfoManager + * @see \Drupal\Core\Render\Annotation\RenderElement + * @see \Drupal\Core\Render\Element\RenderElement + * @see plugin_api + * + * @ingroup theme_render + */ +interface ElementInterface extends PluginInspectionInterface { + + /** + * Returns the element properties for this element. + * + * @return array + * An array of element properties. See + * \Drupal\Core\Render\ElementInfoManagerInterface::getInfo() for + * documentation of the standard properties of all elements, and the + * return value format. + */ + public function getInfo(); + +} diff --git a/core/lib/Drupal/Core/Render/Element/FormElement.php b/core/lib/Drupal/Core/Render/Element/FormElement.php new file mode 100644 index 0000000000000000000000000000000000000000..d66cc89d6eaee4812108985a66ed1a4c3491751f --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/FormElement.php @@ -0,0 +1,162 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\FormElement. + */ + +namespace Drupal\Core\Render\Element; + +use Drupal\Core\Form\FormStateInterface; + +/** + * Provides a base class for form render plugins. + * + * @see \Drupal\Core\Render\Annotation\FormElement + * @see \Drupal\Core\Render\Element\FormElementInterface + * @see \Drupal\Core\Render\ElementInfoManager + * @see plugin_api + * + * @ingroup theme_render + */ +abstract class FormElement extends RenderElement implements FormElementInterface { + + /** + * {@inheritdoc} + */ + public static function valueCallback(&$element, $input, FormStateInterface $form_state) { + return NULL; + } + + /** + * 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. + * + * @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; + } + + /** + * #process callback for #pattern form element property. + * + * @param array $element + * An associative array containing the properties and children of the + * generic input element. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * @param array $complete_form + * The complete form structure. + * + * @return array + * The processed element. + * + * @see form_validate_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'; + } + + return $element; + } + + /** + * Adds autocomplete functionality to elements. + * + * This sets up autocomplete functionality for elements with an + * #autocomplete_route_name property, using the #autocomplete_route_parameters + * property if present. + * + * For example, suppose your autocomplete route name is + * 'mymodule.autocomplete' and its path is + * '/mymodule/autocomplete/{a}/{b}'. In a form array, you would create a text + * field with properties: + * @code + * '#autocomplete_route_name' => 'mymodule.autocomplete', + * '#autocomplete_route_parameters' => array('a' => $some_key, 'b' => $some_id), + * @endcode + * If the user types "keywords" in that field, the full path called would be: + * 'mymodule_autocomplete/$some_key/$some_id?q=keywords' + * + * @param array $element + * The form element to process. Properties used: + * - #autocomplete_route_name: A route to be used as callback URL by the + * autocomplete JavaScript library. + * - #autocomplete_route_parameters: The parameters to be used in + * conjunction with the route name. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * @param array $complete_form + * The complete form structure. + * + * @return array + * The form element. + */ + public static function processAutocomplete(&$element, FormStateInterface $form_state, &$complete_form) { + $access = FALSE; + if (!empty($element['#autocomplete_route_name'])) { + $parameters = isset($element['#autocomplete_route_parameters']) ? $element['#autocomplete_route_parameters'] : array(); + + $path = \Drupal::urlGenerator()->generate($element['#autocomplete_route_name'], $parameters); + $access = \Drupal::service('access_manager')->checkNamedRoute($element['#autocomplete_route_name'], $parameters, \Drupal::currentUser()); + } + if ($access) { + $element['#attributes']['class'][] = 'form-autocomplete'; + $element['#attached']['library'][] = 'core/drupal.autocomplete'; + // Provide a data attribute for the JavaScript behavior to bind to. + $element['#attributes']['data-autocomplete-path'] = $path; + } + + return $element; + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/FormElementInterface.php b/core/lib/Drupal/Core/Render/Element/FormElementInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..2bbbde8ecbdafa870f0ac681cb25fde9749533f7 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/FormElementInterface.php @@ -0,0 +1,46 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\FormElementInterface. + */ + +namespace Drupal\Core\Render\Element; + +use Drupal\Core\Form\FormStateInterface; + +/** + * Provides an interface for form element plugins. + * + * Form element plugins are a subset of render elements, specifically + * representing HTML elements that take input as part of a form. Form element + * plugins are discovered via the same mechanism as regular render element + * plugins. See \Drupal\Core\Render\Element\ElementInterface for general + * information about render element plugins. + * + * @see \Drupal\Core\Render\ElementInfoManager + * @see \Drupal\Core\Render\Element\FormElement + * @see \Drupal\Core\Render\Annotation\FormElement + * @see plugin_api + * + * @ingroup theme_render + */ +interface FormElementInterface extends ElementInterface { + + /** + * Determines how user input is mapped to an element's #value property. + * + * @param array $element + * An associative array containing the properties of the element. + * @param mixed $input + * The incoming input to populate the form element. If this is FALSE, + * the element's default value should be returned. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return mixed + * The value to assign to the element. + */ + public static function valueCallback(&$element, $input, FormStateInterface $form_state); + +} diff --git a/core/lib/Drupal/Core/Render/Element/Link.php b/core/lib/Drupal/Core/Render/Element/Link.php new file mode 100644 index 0000000000000000000000000000000000000000..2a06eb697e2ed45da5ebfd2de2235c970f0121ab --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Link.php @@ -0,0 +1,94 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\Link. + */ + +namespace Drupal\Core\Render\Element; + +/** + * Provides a link render element. + * + * @RenderElement("link") + */ +class Link extends RenderElement { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return array( + '#pre_render' => array( + array($class, 'preRenderLink'), + ), + ); + } + + /** + * Pre-render callback: Renders a link into #markup. + * + * Doing so during pre_render gives modules a chance to alter the link parts. + * + * @param array $element + * A structured array whose keys form the arguments to l(): + * - #title: The link text to pass as argument to l(). + * - One of the following + * - #route_name and (optionally) a #route_parameters array; The route + * name and route parameters which will be passed into the link + * generator. + * - #href: The system path or URL to pass as argument to l(). + * - #options: (optional) An array of options to pass to l() or the link + * generator. + * + * @return array + * The passed-in element containing a rendered link in '#markup'. + */ + public static function preRenderLink($element) { + // By default, link options to pass to l() are normally set in #options. + $element += array('#options' => array()); + // However, within the scope of renderable elements, #attributes is a valid + // way to specify attributes, too. Take them into account, but do not override + // attributes from #options. + if (isset($element['#attributes'])) { + $element['#options'] += array('attributes' => array()); + $element['#options']['attributes'] += $element['#attributes']; + } + + // This #pre_render callback can be invoked from inside or outside of a Form + // API context, and depending on that, a HTML ID may be already set in + // different locations. #options should have precedence over Form API's #id. + // #attributes have been taken over into #options above already. + if (isset($element['#options']['attributes']['id'])) { + $element['#id'] = $element['#options']['attributes']['id']; + } + elseif (isset($element['#id'])) { + $element['#options']['attributes']['id'] = $element['#id']; + } + + // Conditionally invoke ajax_pre_render_element(), if #ajax is set. + if (isset($element['#ajax']) && !isset($element['#ajax_processed'])) { + // If no HTML ID was found above, automatically create one. + if (!isset($element['#id'])) { + $element['#id'] = $element['#options']['attributes']['id'] = drupal_html_id('ajax-link'); + } + // If #ajax['path] was not specified, use the href as Ajax request URL. + if (!isset($element['#ajax']['path'])) { + $element['#ajax']['path'] = $element['#href']; + $element['#ajax']['options'] = $element['#options']; + } + $element = ajax_pre_render_element($element); + } + + if (isset($element['#route_name'])) { + $element['#route_parameters'] = empty($element['#route_parameters']) ? array() : $element['#route_parameters']; + $element['#markup'] = \Drupal::linkGenerator()->generate($element['#title'], $element['#route_name'], $element['#route_parameters'], $element['#options']); + } + else { + $element['#markup'] = l($element['#title'], $element['#href'], $element['#options']); + } + return $element; + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/MachineName.php b/core/lib/Drupal/Core/Render/Element/MachineName.php new file mode 100644 index 0000000000000000000000000000000000000000..6301f3527bb122485451d1199a3782e8635575e6 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/MachineName.php @@ -0,0 +1,226 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\MachineName. + */ + +namespace Drupal\Core\Render\Element; + +use Drupal\Component\Utility\NestedArray; +use Drupal\Component\Utility\SafeMarkup; +use Drupal\Core\Form\FormStateInterface; + +/** + * Provides a machine name render element. + * + * Provides a form element to enter a machine name, which is validated to ensure + * that the name is unique and does not contain disallowed characters. All + * disallowed characters are replaced with a replacement character via + * JavaScript. + * + * @FormElement("machine_name") + */ +class MachineName extends Textfield { + + /** + * {@inheritdoc} + */ + public function getInfo() { + $class = get_class($this); + return array( + '#input' => TRUE, + '#default_value' => NULL, + '#required' => TRUE, + '#maxlength' => 64, + '#size' => 60, + '#autocomplete_route_name' => FALSE, + '#process' => array( + array($class, 'processMachineName'), + array($class, 'processAutocomplete'), + array($class, 'processAjaxForm'), + ), + '#element_validate' => array( + array($class, 'validateMachineName'), + ), + '#pre_render' => array( + array($class, 'preRenderTextfield'), + ), + '#theme' => 'input__textfield', + '#theme_wrappers' => array('form_element'), + ); + } + + /** + * {@inheritdoc} + */ + public static function valueCallback(&$element, $input, FormStateInterface $form_state) { + return NULL; + } + + /** + * Processes a machine-readable name form element. + * + * @param array $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) 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) Maximum allowed length of the machine name. + * Defaults to 64. + * - #disabled: (optional) Should be set to TRUE if an existing machine + * name must not be changed after initial creation. + * @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 processMachineName(&$element, FormStateInterface $form_state, &$complete_form) { + // 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['complete_form'], $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['complete_form'], $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. + * + * This checks that the submitted value: + * - Does not contain the replacement character only. + * - Does not contain disallowed characters. + * - Is unique; i.e., does not already exist. + * - Does not exceed the maximum length (via #maxlength). + * - Cannot be changed after creation (via #disabled). + */ + public static function validateMachineName(&$element, FormStateInterface $form_state, &$complete_form) { + // 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.')); + } + } + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/RenderElement.php b/core/lib/Drupal/Core/Render/Element/RenderElement.php new file mode 100644 index 0000000000000000000000000000000000000000..cf4d4546dd698b03a851670c907d18854973e7f4 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/RenderElement.php @@ -0,0 +1,85 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\RenderElement. + */ + +namespace Drupal\Core\Render\Element; + +use Drupal\Core\Plugin\PluginBase; +use Drupal\Core\Render\Element; + +/** + * Provides a base class for element render plugins. + * + * @see \Drupal\Core\Render\Annotation\RenderElement + * @see \Drupal\Core\Render\ElementInterface + * @see \Drupal\Core\Render\ElementInfoManager + * @see plugin_api + * + * @ingroup theme_render + */ +abstract class RenderElement extends PluginBase implements ElementInterface { + + /** + * Adds members of this group as actual elements for rendering. + * + * @param array $element + * An associative array containing the properties and children of the + * element. + * + * @return array + * The modified element with all group members. + */ + public static function preRenderGroup($element) { + // The element may be rendered outside of a Form API context. + if (!isset($element['#parents']) || !isset($element['#groups'])) { + return $element; + } + + // Inject group member elements belonging to this group. + $parents = implode('][', $element['#parents']); + $children = Element::children($element['#groups'][$parents]); + if (!empty($children)) { + foreach ($children as $key) { + // Break references and indicate that the element should be rendered as + // group member. + $child = (array) $element['#groups'][$parents][$key]; + $child['#group_details'] = TRUE; + // Inject the element as new child element. + $element[] = $child; + + $sort = TRUE; + } + // Re-sort the element's children if we injected group member elements. + if (isset($sort)) { + $element['#sorted'] = FALSE; + } + } + + if (isset($element['#group'])) { + // Contains form element summary functionalities. + $element['#attached']['library'][] = 'core/drupal.form'; + + $group = $element['#group']; + // If this element belongs to a group, but the group-holding element does + // not exist, we need to render it (at its original location). + if (!isset($element['#groups'][$group]['#group_exists'])) { + // Intentionally empty to clarify the flow; we simply return $element. + } + // If we injected this element into the group, then we want to render it. + elseif (!empty($element['#group_details'])) { + // Intentionally empty to clarify the flow; we simply return $element. + } + // Otherwise, this element belongs to a group and the group exists, so we do + // not render it. + elseif (Element::children($element['#groups'][$group])) { + $element['#printed'] = TRUE; + } + } + + return $element; + } + +} diff --git a/core/lib/Drupal/Core/Render/Element/Textfield.php b/core/lib/Drupal/Core/Render/Element/Textfield.php new file mode 100644 index 0000000000000000000000000000000000000000..d687a2cf8c2680f0fbeb237cad9d9344fbd39dfd --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/Textfield.php @@ -0,0 +1,75 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\Element\Textfield. + */ + +namespace Drupal\Core\Render\Element; + +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element; + +/** + * Provides a one-line text field form element. + * + * @FormElement("textfield") + */ +class Textfield 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'), + array($class, 'processPattern'), + array($class, 'processGroup'), + ), + '#pre_render' => array( + array($class, 'preRenderTextfield'), + array($class, 'preRenderGroup'), + ), + '#theme' => 'input__textfield', + '#theme_wrappers' => array('form_element'), + ); + } + + /** + * {@inheritdoc} + */ + public static function valueCallback(&$element, $input, FormStateInterface $form_state) { + if ($input !== FALSE && $input !== NULL) { + // Equate $input to the form value to ensure it's marked for + // validation. + return str_replace(array("\r", "\n"), '', $input); + } + } + + /** + * Prepares a #type 'textfield' 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 preRenderTextfield($element) { + $element['#attributes']['type'] = 'text'; + Element::setAttributes($element, array('id', 'name', 'value', 'size', 'maxlength', 'placeholder')); + _form_set_attributes($element, array('form-text')); + + return $element; + } + +} diff --git a/core/lib/Drupal/Core/Render/ElementInfo.php b/core/lib/Drupal/Core/Render/ElementInfo.php deleted file mode 100644 index 8d6a3f283c0e9f8c522f559c1ca6462532dde860..0000000000000000000000000000000000000000 --- a/core/lib/Drupal/Core/Render/ElementInfo.php +++ /dev/null @@ -1,65 +0,0 @@ -<?php - -/** - * @file - * Contains \Drupal\Core\Render\ElementInfo. - */ - -namespace Drupal\Core\Render; - -use Drupal\Core\Extension\ModuleHandlerInterface; - -/** - * Provides the default element info implementation. - */ -class ElementInfo implements ElementInfoInterface { - - /** - * The module handler. - * - * @var \Drupal\Core\Extension\ModuleHandlerInterface $module_handler - */ - protected $moduleHandler; - - /** - * Stores the available element information - * - * @var array - */ - protected $elementInfo; - - /** - * Constructs a new ElementInfo instance. - * - * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler - * The module handler. - */ - public function __construct(ModuleHandlerInterface $module_handler) { - $this->moduleHandler = $module_handler; - } - - /** - * {@inheritdoc} - */ - public function getInfo($type) { - if (!isset($this->elementInfo)) { - $this->elementInfo = $this->buildInfo(); - } - return isset($this->elementInfo[$type]) ? $this->elementInfo[$type] : array(); - } - - /** - * Builds up all element information. - */ - protected function buildInfo() { - $info = $this->moduleHandler->invokeAll('element_info'); - foreach ($info as $element_type => $element) { - $info[$element_type]['#type'] = $element_type; - } - // Allow modules to alter the element type defaults. - $this->moduleHandler->alter('element_info', $info); - - return $info; - } - -} diff --git a/core/lib/Drupal/Core/Render/ElementInfoInterface.php b/core/lib/Drupal/Core/Render/ElementInfoInterface.php deleted file mode 100644 index ad05aebdfb3a6cf02aa794a785cc46840c18e470..0000000000000000000000000000000000000000 --- a/core/lib/Drupal/Core/Render/ElementInfoInterface.php +++ /dev/null @@ -1,51 +0,0 @@ -<?php - -/** - * @file - * Contains \Drupal\Core\Render\ElementInfoInterface. - */ - -namespace Drupal\Core\Render; - -/** - * Defines available render array element types. - */ -interface ElementInfoInterface { - - /** - * Retrieves the default properties for the defined element type. - * - * Each of the form element types defined by this hook is assumed to have - * a matching theme function, e.g. theme_elementtype(), which should be - * registered with hook_theme() as normal. - * - * For more information about custom element types see the explanation at - * http://drupal.org/node/169815. - * - * @param string $type - * An element type as defined by hook_element_info(). - * - * @return array - * An associative array describing the element types being defined. The array - * contains a sub-array for each element type, with the machine-readable type - * name as the key. Each sub-array has a number of possible attributes: - * - "#input": boolean indicating whether or not this element carries a value - * (even if it's hidden). - * - "#process": array of callback functions taking $element, $form_state, - * and $complete_form. - * - "#after_build": array of callables taking $element and $form_state. - * - "#validate": array of callback functions taking $form and $form_state. - * - "#element_validate": array of callback functions taking $element and - * $form_state. - * - "#pre_render": array of callables taking $element. - * - "#post_render": array of callables taking $children and $element. - * - "#submit": array of callback functions taking $form and $form_state. - * - "#title_display": optional string indicating if and how #title should be - * displayed, see the form-element template and theme_form_element_label(). - * - * @see hook_element_info() - * @see hook_element_info_alter() - */ - public function getInfo($type); - -} diff --git a/core/lib/Drupal/Core/Render/ElementInfoManager.php b/core/lib/Drupal/Core/Render/ElementInfoManager.php new file mode 100644 index 0000000000000000000000000000000000000000..1dd2be4e318213a92860025bca33416be7bba995 --- /dev/null +++ b/core/lib/Drupal/Core/Render/ElementInfoManager.php @@ -0,0 +1,100 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\ElementInfoManager. + */ + +namespace Drupal\Core\Render; + +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Plugin\DefaultPluginManager; +use Drupal\Core\Render\Element\FormElementInterface; + +/** + * Provides a plugin manager for element plugins. + * + * @see \Drupal\Core\Render\Annotation\RenderElement + * @see \Drupal\Core\Render\Annotation\FormElement + * @see \Drupal\Core\Render\Element\RenderElement + * @see \Drupal\Core\Render\Element\FormElement + * @see \Drupal\Core\Render\Element\ElementInterface + * @see \Drupal\Core\Render\Element\FormElementInterface + * @see plugin_api + */ +class ElementInfoManager extends DefaultPluginManager implements ElementInfoManagerInterface { + + /** + * Stores the available element information. + * + * @var array + */ + protected $elementInfo; + + /** + * Constructs a ElementInfoManager object. + * + * @param \Traversable $namespaces + * An object that implements \Traversable which contains the root paths + * keyed by the corresponding namespace to look for plugin implementations. + * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend + * Cache backend instance to use. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler to invoke the alter hook with. + */ + public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) { + $this->setCacheBackend($cache_backend, 'element_info'); + + parent::__construct('Element', $namespaces, $module_handler, 'Drupal\Core\Render\Annotation\RenderElement'); + } + + /** + * {@inheritdoc} + */ + public function getInfo($type) { + if (!isset($this->elementInfo)) { + $this->elementInfo = $this->buildInfo(); + } + return isset($this->elementInfo[$type]) ? $this->elementInfo[$type] : array(); + } + + /** + * Builds up all element information. + */ + protected function buildInfo() { + // @todo Remove this hook once all elements are converted to plugins in + // https://www.drupal.org/node/2311393. + $info = $this->moduleHandler->invokeAll('element_info'); + + foreach ($this->getDefinitions() as $element_type => $definition) { + $element = $this->createInstance($element_type); + $element_info = $element->getInfo(); + + // If this is element is to be used exclusively in a form, denote that it + // will receive input, and assign the value callback. + if ($element instanceof FormElementInterface) { + $element_info['#input'] = TRUE; + $element_info['#value_callback'] = array($definition['class'], 'valueCallback'); + } + $info[$element_type] = $element_info; + } + foreach ($info as $element_type => $element) { + $info[$element_type]['#type'] = $element_type; + } + // Allow modules to alter the element type defaults. + $this->moduleHandler->alter('element_info', $info); + + return $info; + } + + /** + * {@inheritdoc} + * + * @return \Drupal\Core\Render\Element\ElementInterface + */ + public function createInstance($plugin_id, array $configuration = array()) { + return parent::createInstance($plugin_id, $configuration); + } + +} diff --git a/core/lib/Drupal/Core/Render/ElementInfoManagerInterface.php b/core/lib/Drupal/Core/Render/ElementInfoManagerInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..d32acd690f93598349647c6905e55c0d5cec9d2f --- /dev/null +++ b/core/lib/Drupal/Core/Render/ElementInfoManagerInterface.php @@ -0,0 +1,55 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Render\ElementInfoManagerInterface. + */ + +namespace Drupal\Core\Render; + +/** + * Collects available render array element types. + */ +interface ElementInfoManagerInterface { + + /** + * Retrieves the default properties for the defined element type. + * + * Each of the form element types defined by this hook is assumed to have + * a matching theme hook, which should be registered with hook_theme() as + * normal. + * + * For more information about custom element types see the explanation at + * http://drupal.org/node/169815. + * + * @param string $type + * An element type as defined by hook_element_info() or the machine name + * of an element type plugin. + * + * @return array + * An associative array describing the element types being defined. The + * array contains a sub-array for each element type, with the + * machine-readable type name as the key. Each sub-array has a number of + * possible attributes: + * - #input: boolean indicating whether or not this element carries a value + * (even if it's hidden). + * - #process: array of callback functions taking $element, $form_state, + * and $complete_form. + * - #after_build: array of callables taking $element and $form_state. + * - #validate: array of callback functions taking $form and $form_state. + * - #element_validate: array of callback functions taking $element and + * $form_state. + * - #pre_render: array of callables taking $element. + * - #post_render: array of callables taking $children and $element. + * - #submit: array of callback functions taking $form and $form_state. + * - #title_display: optional string indicating if and how #title should be + * displayed (see form-element.html.twig). + * + * @see hook_element_info() + * @see hook_element_info_alter() + * @see \Drupal\Core\Render\Element\ElementInterface + * @see \Drupal\Core\Render\Element\ElementInterface::getInfo() + */ + public function getInfo($type); + +} diff --git a/core/lib/Drupal/Core/Render/Plugin/README.txt b/core/lib/Drupal/Core/Render/Plugin/README.txt new file mode 100644 index 0000000000000000000000000000000000000000..f761e0198cef57dbae101045b12e2bc350d208a9 --- /dev/null +++ b/core/lib/Drupal/Core/Render/Plugin/README.txt @@ -0,0 +1,4 @@ +@todo This must be here because DrupalKernel will only allow namespaces to + provide plugins if there is a Plugin subdirectory, and git does not allow + empty subdirectories. This file should be removed once + https://www.drupal.org/node/2309889 is fixed. diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php index 3d52bbdd598146435310be9a9811663b5dce5daa..86336f66d5c48d87bebe9180040182ecd03933bc 100644 --- a/core/modules/system/system.api.php +++ b/core/modules/system/system.api.php @@ -193,11 +193,14 @@ function callback_queue_worker($queue_item_data) { * specify their default values. The values returned by this hook will be * merged with the elements returned by form constructor implementations and so * can return defaults for any Form APIs keys in addition to those explicitly - * documented by \Drupal\Core\Render\ElementInfoInterface::getInfo(). + * documented by \Drupal\Core\Render\ElementInfoManagerInterface::getInfo(). * * @return array * An associative array with structure identical to that of the return value - * of \Drupal\Core\Render\ElementInfoInterface::getInfo(). + * of \Drupal\Core\Render\ElementInfoManagerInterface::getInfo(). + * + * @deprecated Use an annotated class instead, see + * \Drupal\Core\Render\Element\ElementInterface. * * @see hook_element_info_alter() * @see system_element_info() @@ -217,7 +220,7 @@ function hook_element_info() { * * @param array $types * An associative array with structure identical to that of the return value - * of \Drupal\Core\Render\ElementInfoInterface::getInfo(). + * of \Drupal\Core\Render\ElementInfoManagerInterface::getInfo(). * * @see hook_element_info() */ diff --git a/core/modules/system/system.module b/core/modules/system/system.module index cad83e53265365aca72d5e9f282e96fdfaf3c335..6875449ff18b21c5efe358abaa87c4788773dc3c 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -368,16 +368,6 @@ function system_element_info() { '#pre_render' => array('form_pre_render_image_button'), '#theme_wrappers' => array('input__image_button'), ); - $types['textfield'] = array( - '#input' => TRUE, - '#size' => 60, - '#maxlength' => 128, - '#autocomplete_route_name' => FALSE, - '#process' => array('form_process_autocomplete', 'ajax_process_form', 'form_process_pattern', 'form_process_group'), - '#pre_render' => array('form_pre_render_textfield', 'form_pre_render_group'), - '#theme' => 'input__textfield', - '#theme_wrappers' => array('form_element'), - ); $types['tel'] = array( '#input' => TRUE, '#size' => 30, @@ -448,19 +438,6 @@ function system_element_info() { '#theme' => 'input__color', '#theme_wrappers' => array('form_element'), ); - $types['machine_name'] = array( - '#input' => TRUE, - '#default_value' => NULL, - '#required' => TRUE, - '#maxlength' => 64, - '#size' => 60, - '#autocomplete_route_name' => FALSE, - '#process' => array('form_process_machine_name', 'form_process_autocomplete', 'ajax_process_form'), - '#element_validate' => array('form_validate_machine_name'), - '#pre_render' => array('form_pre_render_textfield'), - '#theme' => 'input__textfield', - '#theme_wrappers' => array('form_element'), - ); $types['password'] = array( '#input' => TRUE, '#size' => 60, @@ -586,9 +563,6 @@ function system_element_info() { $types['value'] = array( '#input' => TRUE, ); - $types['link'] = array( - '#pre_render' => array('drupal_pre_render_link'), - ); $types['fieldset'] = array( '#value' => NULL, '#process' => array('form_process_group', 'ajax_process_form'), diff --git a/core/modules/system/theme.api.php b/core/modules/system/theme.api.php index 85eb7283a7a3def1e081f4479aca1c4962691531..fabde88462aba28ebb2e931ce96f50a9553e892d 100644 --- a/core/modules/system/theme.api.php +++ b/core/modules/system/theme.api.php @@ -163,11 +163,21 @@ * requests and the CSS files used to style that markup. In order to ensure that * a theme can completely customize the markup, module developers should avoid * directly writing HTML markup for pages, blocks, and other user-visible output - * in their modules, and instead return structured "render arrays" (described - * below). Doing this also increases usability, by ensuring that the markup used - * for similar functionality on different areas of the site is the same, which - * gives users fewer user interface patterns to learn. + * in their modules, and instead return structured "render arrays" (see @ref + * arrays below). Doing this also increases usability, by ensuring that the + * markup used for similar functionality on different areas of the site is the + * same, which gives users fewer user interface patterns to learn. * + * For further information on the Theme and Render APIs, see: + * - https://drupal.org/documentation/theme + * - https://drupal.org/node/722174 + * - https://drupal.org/node/933976 + * - https://drupal.org/node/930760 + * + * @todo Check these links. Some are for Drupal 7, and might need updates for + * Drupal 8. + * + * @section arrays Render arrays * The core structure of the Render API is the render array, which is a * hierarchical associative array containing data to be rendered and properties * describing how the data should be rendered. A render array that is returned @@ -194,11 +204,8 @@ * - #type: Specifies that the array contains data and options for a particular * type of "render element" (examples: 'form', for an HTML form; 'textfield', * 'submit', and other HTML form element types; 'table', for a table with - * rows, columns, and headers). Modules define render elements by implementing - * hook_element_info(), which specifies the properties that are used in render - * arrays to provide the data and options, and default values for these - * properties. Look through implementations of hook_element_info() to discover - * what render elements are available. + * rows, columns, and headers). See @ref elements below for more on render + * element types. * - #theme: Specifies that the array contains data to be themed by a particular * theme hook. Modules define theme hooks by implementing hook_theme(), which * specifies the input "variables" used to provide data and options; if a @@ -214,15 +221,29 @@ * normally preferable to use #theme or #type instead, so that the theme can * customize the markup. * - * For further information on the Theme and Render APIs, see: - * - https://drupal.org/documentation/theme - * - https://drupal.org/developing/modules/8 - * - https://drupal.org/node/722174 - * - https://drupal.org/node/933976 - * - https://drupal.org/node/930760 + * @section elements Render elements + * Render elements are defined by Drupal core and modules. The primary way to + * define a render element is to create a render element plugin. There are + * two types of render element plugins: + * - Generic elements: Generic render element plugins implement + * \Drupal\Core\Render\Element\ElementInterface, are annotated with + * \Drupal\Core\Render\Annotation\RenderElement annotation, go in plugin + * namespace Element, and generally extend the + * \Drupal\Core\Render\Element\RenderElement base class. + * - Form input elements: Render elements representing form input elements + * implement \Drupal\Core\Render\Element\FormElementInterface, are annotated + * with \Drupal\Core\Render\Annotation\FormElement annotation, go in plugin + * namespace Element, and generally extend the + * \Drupal\Core\Render\Element\FormElement base class. + * See the @link plugin_api Plugin API topic @endlink for general information + * on plugins, and look for classes with the RenderElement or FormElement + * annotation to discover what render elements are available. + * + * Modules can also currently define render elements by implementing + * hook_element_info(), although defining a plugin is preferred. + * properties. Look through implementations of hook_element_info() to discover + * elements defined this way. * - * @todo Check these links. Some are for Drupal 7, and might need updates for - * Drupal 8. * @see themeable * * @} diff --git a/core/tests/Drupal/Tests/Core/Render/ElementInfoManagerTest.php b/core/tests/Drupal/Tests/Core/Render/ElementInfoManagerTest.php new file mode 100644 index 0000000000000000000000000000000000000000..fa9cbb3a23fcafb40578d4e090d2f23121402f12 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Render/ElementInfoManagerTest.php @@ -0,0 +1,197 @@ +<?php + +/** + * @file + * Contains \Drupal\Tests\Core\Render\ElementInfoManagerTest. + */ + +namespace Drupal\Tests\Core\Render; + +use Drupal\Core\Render\ElementInfoManager; +use Drupal\Tests\UnitTestCase; + +/** + * @coversDefaultClass \Drupal\Core\Render\ElementInfoManager + * @group Render + */ +class ElementInfoManagerTest extends UnitTestCase { + + /** + * The class under test. + * + * @var \Drupal\Core\Render\ElementInfoManagerInterface + */ + protected $elementInfo; + + /** + * The cache backend to use. + * + * @var \Drupal\Core\Cache\CacheBackendInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $cache; + + /** + * The mocked module handler. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $moduleHandler; + + /** + * {@inheritdoc} + * + * @covers ::__construct + */ + protected function setUp() { + $this->cache = $this->getMock('Drupal\Core\Cache\CacheBackendInterface'); + $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface'); + + $this->elementInfo = new ElementInfoManager(new \ArrayObject(), $this->cache, $this->moduleHandler); + } + + /** + * Tests the getInfo method. + * + * @covers ::getInfo + * @covers ::buildInfo + * + * @dataProvider providerTestGetInfo + */ + public function testGetInfo($type, $expected_info, $element_info, callable $alter_callback = NULL) { + $this->moduleHandler->expects($this->once()) + ->method('invokeAll') + ->with('element_info') + ->will($this->returnValue($element_info)); + $this->moduleHandler->expects($this->once()) + ->method('alter') + ->with('element_info', $this->anything()) + ->will($this->returnCallback($alter_callback ?: function($info) { + return $info; + })); + + $this->assertEquals($expected_info, $this->elementInfo->getInfo($type)); + } + + /** + * Provides tests data for getInfo. + * + * @return array + */ + public function providerTestGetInfo() { + $data = array(); + // Provide an element and expect it is returned. + $data[] = array( + 'page', + array( + '#type' => 'page', + '#show_messages' => TRUE, + '#theme' => 'page', + ), + array('page' => array( + '#show_messages' => TRUE, + '#theme' => 'page', + )), + ); + // Provide an element but request an non existent one. + $data[] = array( + 'form', + array( + ), + array('page' => array( + '#show_messages' => TRUE, + '#theme' => 'page', + )), + ); + // Provide an element and alter it to ensure it is altered. + $data[] = array( + 'page', + array( + '#type' => 'page', + '#show_messages' => TRUE, + '#theme' => 'page', + '#number' => 597219, + ), + array('page' => array( + '#show_messages' => TRUE, + '#theme' => 'page', + )), + function ($alter_name, array &$info) { + $info['page']['#number'] = 597219; + } + ); + return $data; + } + + /** + * Tests the getInfo() method when render element plugins are used. + * + * @covers ::getInfo + * @covers ::buildInfo + * + * @dataProvider providerTestGetInfoElementPlugin + */ + public function testGetInfoElementPlugin($plugin_class, $expected_info) { + $this->moduleHandler->expects($this->once()) + ->method('invokeAll') + ->with('element_info') + ->willReturn(array()); + $this->moduleHandler->expects($this->once()) + ->method('alter') + ->with('element_info', $this->anything()) + ->will($this->returnArgument(0)); + + $plugin = $this->getMock($plugin_class); + $plugin->expects($this->once()) + ->method('getInfo') + ->willReturn(array( + '#show_messages' => TRUE, + '#theme' => 'page', + )); + + $element_info = $this->getMockBuilder('Drupal\Core\Render\ElementInfoManager') + ->setConstructorArgs(array(new \ArrayObject(), $this->cache, $this->moduleHandler)) + ->setMethods(array('getDefinitions', 'createInstance')) + ->getMock(); + $element_info->expects($this->once()) + ->method('createInstance') + ->with('page') + ->willReturn($plugin); + $element_info->expects($this->once()) + ->method('getDefinitions') + ->willReturn(array( + 'page' => array('class' => 'TestElementPlugin'), + )); + + $this->assertEquals($expected_info, $element_info->getInfo('page')); + } + + /** + * Provides tests data for testGetInfoElementPlugin(). + * + * @return array + */ + public function providerTestGetInfoElementPlugin() { + $data = array(); + $data[] = array( + 'Drupal\Core\Render\Element\ElementInterface', + array( + '#type' => 'page', + '#show_messages' => TRUE, + '#theme' => 'page', + ), + ); + + $data[] = array( + 'Drupal\Core\Render\Element\FormElementInterface', + array( + '#type' => 'page', + '#show_messages' => TRUE, + '#theme' => 'page', + '#input' => TRUE, + '#value_callback' => array('TestElementPlugin', 'valueCallback'), + ), + ); + return $data; + } + +} diff --git a/core/tests/Drupal/Tests/Core/Render/ElementInfoTest.php b/core/tests/Drupal/Tests/Core/Render/ElementInfoTest.php deleted file mode 100644 index 86878f4322ae7a4d0591355542c4fca3947b5c2e..0000000000000000000000000000000000000000 --- a/core/tests/Drupal/Tests/Core/Render/ElementInfoTest.php +++ /dev/null @@ -1,118 +0,0 @@ -<?php - -/** - * @file - * Contains \Drupal\Tests\Core\Render\ElementInfoTest. - */ - -namespace Drupal\Tests\Core\Render; - -use Drupal\Core\Render\ElementInfo; -use Drupal\Core\Render\ElementInfoInterface; -use Drupal\Tests\UnitTestCase; - -/** - * @coversDefaultClass \Drupal\Core\Render\ElementInfo - * @group Render - */ -class ElementInfoTest extends UnitTestCase { - - /** - * The class under test. - * - * @var \Drupal\Core\Render\ElementInfoInterface - */ - protected $elementInfo; - - /** - * The mocked module handler. - * - * @var \Drupal\Core\Extension\ModuleHandlerInterface|\PHPUnit_Framework_MockObject_MockObject - */ - protected $moduleHandler; - - /** - * {@inheritdoc} - * - * @covers ::__construct - */ - protected function setUp() { - $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface'); - - $this->elementInfo = new ElementInfo($this->moduleHandler); - } - - /** - * Tests the getInfo method. - * - * @covers ::getInfo - * @covers ::buildInfo - * - * @dataProvider providerTestGetInfo - */ - public function testGetInfo($type, $expected_info, $element_info, callable $alter_callback = NULL) { - $this->moduleHandler->expects($this->once()) - ->method('invokeAll') - ->with('element_info') - ->will($this->returnValue($element_info)); - $this->moduleHandler->expects($this->once()) - ->method('alter') - ->with('element_info', $this->anything()) - ->will($this->returnCallback($alter_callback ?: function($info) { - return $info; - })); - - $this->assertEquals($expected_info, $this->elementInfo->getInfo($type)); - } - - /** - * Provides tests data for getInfo. - * - * @return array - */ - public function providerTestGetInfo() { - $data = array(); - // Provide an element and expect it is returned. - $data[] = array( - 'page', - array( - '#type' => 'page', - '#show_messages' => TRUE, - '#theme' => 'page', - ), - array('page' => array( - '#show_messages' => TRUE, - '#theme' => 'page', - )), - ); - // Provide an element but request an non existent one. - $data[] = array( - 'form', - array( - ), - array('page' => array( - '#show_messages' => TRUE, - '#theme' => 'page', - )), - ); - // Provide an element and alter it to ensure it is altered. - $data[] = array( - 'page', - array( - '#type' => 'page', - '#show_messages' => TRUE, - '#theme' => 'page', - '#number' => 597219, - ), - array('page' => array( - '#show_messages' => TRUE, - '#theme' => 'page', - )), - function ($alter_name, array &$info) { - $info['page']['#number'] = 597219; - } - ); - return $data; - } - -}