RenderElement.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. <?php
  2. namespace Drupal\Core\Render\Element;
  3. use Drupal\Core\Form\FormBuilderInterface;
  4. use Drupal\Core\Form\FormStateInterface;
  5. use Drupal\Core\Plugin\PluginBase;
  6. use Drupal\Core\Render\BubbleableMetadata;
  7. use Drupal\Core\Render\Element;
  8. use Drupal\Core\Url;
  9. /**
  10. * Provides a base class for render element plugins.
  11. *
  12. * Render elements are referenced in render arrays; see the
  13. * @link theme_render Render API topic @endlink for an overview of render
  14. * arrays and render elements.
  15. *
  16. * The elements of render arrays are divided up into properties (whose keys
  17. * start with #) and children (whose keys do not start with #). The properties
  18. * provide data or settings that are used in rendering. Some properties are
  19. * specific to a particular type of render element, some are available for any
  20. * render element, and some are available for any form input element. A list of
  21. * the properties that are available for all render elements follows; the
  22. * properties that are for all form elements are documented on
  23. * \Drupal\Core\Render\Element\FormElement, and properties specific to a
  24. * particular element are documented on that element's class. See the
  25. * @link theme_render Render API topic @endlink for a list of the most
  26. * commonly-used properties.
  27. *
  28. * Many of the properties are strings that are displayed to users. These
  29. * strings, if they are literals provided by your module, should be
  30. * internationalized and translated; see the
  31. * @link i18n Internationalization topic @endlink for more information. Note
  32. * that although in the properties list that follows, they are designated to be
  33. * of type string, they would generally end up being
  34. * \Drupal\Core\StringTranslation\TranslatableMarkup objects instead.
  35. *
  36. * Here is the list of the properties used during the rendering of all render
  37. * elements:
  38. * - #access: (bool) Whether the element is accessible or not. When FALSE,
  39. * the element is not rendered and user-submitted values are not taken
  40. * into consideration.
  41. * - #access_callback: A callable or function name to call to check access.
  42. * Argument: element.
  43. * - #allowed_tags: (array) Array of allowed HTML tags for XSS filtering of
  44. * #markup, #prefix, #suffix, etc.
  45. * - #attached: (array) Array of attachments associated with the element.
  46. * See the "Attaching libraries in render arrays" section of the
  47. * @link theme_render Render API topic @endlink for an overview, and
  48. * \Drupal\Core\Render\AttachmentsResponseProcessorInterface::processAttachments
  49. * for a list of what this can contain. Besides this list, it may also contain
  50. * a 'placeholders' element; see the Placeholders section of the
  51. * @link theme_render Render API topic @endlink for an overview.
  52. * - #attributes: (array) HTML attributes for the element. The first-level
  53. * keys are the attribute names, such as 'class', and the attributes are
  54. * usually given as an array of string values to apply to that attribute
  55. * (the rendering system will concatenate them together into a string in
  56. * the HTML output).
  57. * - #cache: (array) Cache information. See the Caching section of the
  58. * @link theme_render Render API topic @endlink for more information.
  59. * - #children: (array, internal) Array of child elements of this element.
  60. * Set and used during the rendering process.
  61. * - #create_placeholder: (bool) TRUE if the element has placeholders that
  62. * are generated by #lazy_builder callbacks. Set internally during rendering
  63. * in some cases. See also #attached.
  64. * - #defaults_loaded: (bool) Set to TRUE during rendering when the defaults
  65. * for the element #type have been added to the element.
  66. * - #id: (string) The HTML ID on the element. This is automatically set for
  67. * form elements, but not for all render elements; you can override the
  68. * default value or add an ID by setting this property.
  69. * - #lazy_builder: (array) Array whose first element is a lazy building
  70. * callback (callable), and whose second is an array of scalar arguments to
  71. * the callback. To use lazy building, the element array must be very
  72. * simple: no properties except #lazy_builder, #cache, #weight, and
  73. * #create_placeholder, and no children. A lazy builder callback typically
  74. * generates #markup and/or placeholders; see the Placeholders section of the
  75. * @link theme_render Render API topic @endlink for information about
  76. * placeholders.
  77. * - #markup: (string) During rendering, this will be set to the HTML markup
  78. * output. It can also be set on input, as a fallback if there is no
  79. * theming for the element. This will be filtered for XSS problems during
  80. * rendering; see also #plain_text and #allowed_tags.
  81. * - #plain_text: (string) Elements can set this instead of #markup. All HTML
  82. * tags will be escaped in this text, and if both #plain_text and #markup
  83. * are provided, #plain_text is used.
  84. * - #post_render: (array) Array of callables or function names, which are
  85. * called after the element is rendered. Arguments: rendered element string,
  86. * children.
  87. * - #pre_render: (array) Array of callables or function names, which are
  88. * called just before the element is rendered. Argument: $element.
  89. * Return value: an altered $element.
  90. * - #prefix: (string) Text to render before the entire element output. See
  91. * also #suffix. If it is not already wrapped in a safe markup object, will
  92. * be filtered for XSS safety.
  93. * - #printed: (bool, internal) Set to TRUE when an element and its children
  94. * have been rendered.
  95. * - #render_children: (bool, internal) Set to FALSE by the rendering process
  96. * if the #theme call should be bypassed (normally, the theme is used to
  97. * render the children). Set to TRUE by the rendering process if the children
  98. * should be rendered by rendering each one separately and concatenating.
  99. * - #suffix: (string) Text to render after the entire element output. See
  100. * also #prefix. If it is not already wrapped in a safe markup object, will
  101. * be filtered for XSS safety.
  102. * - #theme: (string) Name of the theme hook to use to render the element.
  103. * A default is generally set for elements; users of the element can
  104. * override this (typically by adding __suggestion suffixes).
  105. * - #theme_wrappers: (array) Array of theme hooks, which are invoked
  106. * after the element and children are rendered, and before #post_render
  107. * functions.
  108. * - #type: (string) The machine name of the type of render/form element.
  109. * - #weight: (float) The sort order for rendering, with lower numbers coming
  110. * before higher numbers. Default if not provided is zero; elements with
  111. * the same weight are rendered in the order they appear in the render
  112. * array.
  113. *
  114. * @see \Drupal\Core\Render\Annotation\RenderElement
  115. * @see \Drupal\Core\Render\ElementInterface
  116. * @see \Drupal\Core\Render\ElementInfoManager
  117. * @see plugin_api
  118. *
  119. * @ingroup theme_render
  120. */
  121. abstract class RenderElement extends PluginBase implements ElementInterface {
  122. /**
  123. * {@inheritdoc}
  124. */
  125. public static function setAttributes(&$element, $class = []) {
  126. if (!empty($class)) {
  127. if (!isset($element['#attributes']['class'])) {
  128. $element['#attributes']['class'] = [];
  129. }
  130. $element['#attributes']['class'] = array_merge($element['#attributes']['class'], $class);
  131. }
  132. // This function is invoked from form element theme functions, but the
  133. // rendered form element may not necessarily have been processed by
  134. // \Drupal::formBuilder()->doBuildForm().
  135. if (!empty($element['#required'])) {
  136. $element['#attributes']['class'][] = 'required';
  137. $element['#attributes']['required'] = 'required';
  138. $element['#attributes']['aria-required'] = 'true';
  139. }
  140. if (isset($element['#parents']) && isset($element['#errors']) && !empty($element['#validated'])) {
  141. $element['#attributes']['class'][] = 'error';
  142. $element['#attributes']['aria-invalid'] = 'true';
  143. }
  144. }
  145. /**
  146. * Adds members of this group as actual elements for rendering.
  147. *
  148. * @param array $element
  149. * An associative array containing the properties and children of the
  150. * element.
  151. *
  152. * @return array
  153. * The modified element with all group members.
  154. */
  155. public static function preRenderGroup($element) {
  156. // The element may be rendered outside of a Form API context.
  157. if (!isset($element['#parents']) || !isset($element['#groups'])) {
  158. return $element;
  159. }
  160. // Inject group member elements belonging to this group.
  161. $parents = implode('][', $element['#parents']);
  162. $children = Element::children($element['#groups'][$parents]);
  163. if (!empty($children)) {
  164. foreach ($children as $key) {
  165. // Break references and indicate that the element should be rendered as
  166. // group member.
  167. $child = (array) $element['#groups'][$parents][$key];
  168. $child['#group_details'] = TRUE;
  169. // Inject the element as new child element.
  170. $element[] = $child;
  171. $sort = TRUE;
  172. }
  173. // Re-sort the element's children if we injected group member elements.
  174. if (isset($sort)) {
  175. $element['#sorted'] = FALSE;
  176. }
  177. }
  178. if (isset($element['#group'])) {
  179. // Contains form element summary functionalities.
  180. $element['#attached']['library'][] = 'core/drupal.form';
  181. $group = $element['#group'];
  182. // If this element belongs to a group, but the group-holding element does
  183. // not exist, we need to render it (at its original location).
  184. if (!isset($element['#groups'][$group]['#group_exists'])) {
  185. // Intentionally empty to clarify the flow; we simply return $element.
  186. }
  187. // If we injected this element into the group, then we want to render it.
  188. elseif (!empty($element['#group_details'])) {
  189. // Intentionally empty to clarify the flow; we simply return $element.
  190. }
  191. // Otherwise, this element belongs to a group and the group exists, so we do
  192. // not render it.
  193. elseif (Element::children($element['#groups'][$group])) {
  194. $element['#printed'] = TRUE;
  195. }
  196. }
  197. return $element;
  198. }
  199. /**
  200. * Form element processing handler for the #ajax form property.
  201. *
  202. * This method is useful for non-input elements that can be used in and
  203. * outside the context of a form.
  204. *
  205. * @param array $element
  206. * An associative array containing the properties of the element.
  207. * @param \Drupal\Core\Form\FormStateInterface $form_state
  208. * The current state of the form.
  209. * @param array $complete_form
  210. * The complete form structure.
  211. *
  212. * @return array
  213. * The processed element.
  214. *
  215. * @see self::preRenderAjaxForm()
  216. */
  217. public static function processAjaxForm(&$element, FormStateInterface $form_state, &$complete_form) {
  218. return static::preRenderAjaxForm($element);
  219. }
  220. /**
  221. * Adds Ajax information about an element to communicate with JavaScript.
  222. *
  223. * If #ajax is set on an element, this additional JavaScript is added to the
  224. * page header to attach the Ajax behaviors. See ajax.js for more information.
  225. *
  226. * @param array $element
  227. * An associative array containing the properties of the element.
  228. * Properties used:
  229. * - #ajax['event']
  230. * - #ajax['prevent']
  231. * - #ajax['url']
  232. * - #ajax['callback']
  233. * - #ajax['options']
  234. * - #ajax['wrapper']
  235. * - #ajax['parameters']
  236. * - #ajax['effect']
  237. * - #ajax['accepts']
  238. *
  239. * @return array
  240. * The processed element with the necessary JavaScript attached to it.
  241. */
  242. public static function preRenderAjaxForm($element) {
  243. // Skip already processed elements.
  244. if (isset($element['#ajax_processed'])) {
  245. return $element;
  246. }
  247. // Initialize #ajax_processed, so we do not process this element again.
  248. $element['#ajax_processed'] = FALSE;
  249. // Nothing to do if there are no Ajax settings.
  250. if (empty($element['#ajax'])) {
  251. return $element;
  252. }
  253. // Add a data attribute to disable automatic refocus after ajax call.
  254. if (!empty($element['#ajax']['disable-refocus'])) {
  255. $element['#attributes']['data-disable-refocus'] = "true";
  256. }
  257. // Add a reasonable default event handler if none was specified.
  258. if (isset($element['#ajax']) && !isset($element['#ajax']['event'])) {
  259. switch ($element['#type']) {
  260. case 'submit':
  261. case 'button':
  262. case 'image_button':
  263. // Pressing the ENTER key within a textfield triggers the click event of
  264. // the form's first submit button. Triggering Ajax in this situation
  265. // leads to problems, like breaking autocomplete textfields, so we bind
  266. // to mousedown instead of click.
  267. // @see https://www.drupal.org/node/216059
  268. $element['#ajax']['event'] = 'mousedown';
  269. // Retain keyboard accessibility by setting 'keypress'. This causes
  270. // ajax.js to trigger 'event' when SPACE or ENTER are pressed while the
  271. // button has focus.
  272. $element['#ajax']['keypress'] = TRUE;
  273. // Binding to mousedown rather than click means that it is possible to
  274. // trigger a click by pressing the mouse, holding the mouse button down
  275. // until the Ajax request is complete and the button is re-enabled, and
  276. // then releasing the mouse button. Set 'prevent' so that ajax.js binds
  277. // an additional handler to prevent such a click from triggering a
  278. // non-Ajax form submission. This also prevents a textfield's ENTER
  279. // press triggering this button's non-Ajax form submission behavior.
  280. if (!isset($element['#ajax']['prevent'])) {
  281. $element['#ajax']['prevent'] = 'click';
  282. }
  283. break;
  284. case 'password':
  285. case 'textfield':
  286. case 'number':
  287. case 'tel':
  288. case 'textarea':
  289. $element['#ajax']['event'] = 'blur';
  290. break;
  291. case 'radio':
  292. case 'checkbox':
  293. case 'select':
  294. case 'date':
  295. $element['#ajax']['event'] = 'change';
  296. break;
  297. case 'link':
  298. $element['#ajax']['event'] = 'click';
  299. break;
  300. default:
  301. return $element;
  302. }
  303. }
  304. // Attach JavaScript settings to the element.
  305. if (isset($element['#ajax']['event'])) {
  306. $element['#attached']['library'][] = 'core/jquery.form';
  307. $element['#attached']['library'][] = 'core/drupal.ajax';
  308. $settings = $element['#ajax'];
  309. // Assign default settings. When 'url' is set to NULL, ajax.js submits the
  310. // Ajax request to the same URL as the form or link destination is for
  311. // someone with JavaScript disabled. This is generally preferred as a way to
  312. // ensure consistent server processing for js and no-js users, and Drupal's
  313. // content negotiation takes care of formatting the response appropriately.
  314. // However, 'url' and 'options' may be set when wanting server processing
  315. // to be substantially different for a JavaScript triggered submission.
  316. $settings += [
  317. 'url' => NULL,
  318. 'options' => ['query' => []],
  319. 'dialogType' => 'ajax',
  320. ];
  321. if (array_key_exists('callback', $settings) && !isset($settings['url'])) {
  322. $settings['url'] = Url::fromRoute('<current>');
  323. // Add all the current query parameters in order to ensure that we build
  324. // the same form on the AJAX POST requests. For example,
  325. // \Drupal\user\AccountForm takes query parameters into account in order
  326. // to hide the password field dynamically.
  327. $settings['options']['query'] += \Drupal::request()->query->all();
  328. $settings['options']['query'][FormBuilderInterface::AJAX_FORM_REQUEST] = TRUE;
  329. }
  330. // @todo Legacy support. Remove in Drupal 8.
  331. if (isset($settings['method']) && $settings['method'] == 'replace') {
  332. $settings['method'] = 'replaceWith';
  333. }
  334. // Convert \Drupal\Core\Url object to string.
  335. if (isset($settings['url']) && $settings['url'] instanceof Url) {
  336. $url = $settings['url']->setOptions($settings['options'])->toString(TRUE);
  337. BubbleableMetadata::createFromRenderArray($element)
  338. ->merge($url)
  339. ->applyTo($element);
  340. $settings['url'] = $url->getGeneratedUrl();
  341. }
  342. else {
  343. $settings['url'] = NULL;
  344. }
  345. unset($settings['options']);
  346. // Add special data to $settings['submit'] so that when this element
  347. // triggers an Ajax submission, Drupal's form processing can determine which
  348. // element triggered it.
  349. // @see _form_element_triggered_scripted_submission()
  350. if (isset($settings['trigger_as'])) {
  351. // An element can add a 'trigger_as' key within #ajax to make the element
  352. // submit as though another one (for example, a non-button can use this
  353. // to submit the form as though a button were clicked). When using this,
  354. // the 'name' key is always required to identify the element to trigger
  355. // as. The 'value' key is optional, and only needed when multiple elements
  356. // share the same name, which is commonly the case for buttons.
  357. $settings['submit']['_triggering_element_name'] = $settings['trigger_as']['name'];
  358. if (isset($settings['trigger_as']['value'])) {
  359. $settings['submit']['_triggering_element_value'] = $settings['trigger_as']['value'];
  360. }
  361. unset($settings['trigger_as']);
  362. }
  363. elseif (isset($element['#name'])) {
  364. // Most of the time, elements can submit as themselves, in which case the
  365. // 'trigger_as' key isn't needed, and the element's name is used.
  366. $settings['submit']['_triggering_element_name'] = $element['#name'];
  367. // If the element is a (non-image) button, its name may not identify it
  368. // uniquely, in which case a match on value is also needed.
  369. // @see _form_button_was_clicked()
  370. if (!empty($element['#is_button']) && empty($element['#has_garbage_value'])) {
  371. $settings['submit']['_triggering_element_value'] = $element['#value'];
  372. }
  373. }
  374. // Convert a simple #ajax['progress'] string into an array.
  375. if (isset($settings['progress']) && is_string($settings['progress'])) {
  376. $settings['progress'] = ['type' => $settings['progress']];
  377. }
  378. // Change progress path to a full URL.
  379. if (isset($settings['progress']['url']) && $settings['progress']['url'] instanceof Url) {
  380. $settings['progress']['url'] = $settings['progress']['url']->toString();
  381. }
  382. $element['#attached']['drupalSettings']['ajax'][$element['#id']] = $settings;
  383. $element['#attached']['drupalSettings']['ajaxTrustedUrl'][$settings['url']] = TRUE;
  384. // Indicate that Ajax processing was successful.
  385. $element['#ajax_processed'] = TRUE;
  386. }
  387. return $element;
  388. }
  389. /**
  390. * Arranges elements into groups.
  391. *
  392. * This method is useful for non-input elements that can be used in and
  393. * outside the context of a form.
  394. *
  395. * @param array $element
  396. * An associative array containing the properties and children of the
  397. * element. Note that $element must be taken by reference here, so processed
  398. * child elements are taken over into $form_state.
  399. * @param \Drupal\Core\Form\FormStateInterface $form_state
  400. * The current state of the form.
  401. * @param array $complete_form
  402. * The complete form structure.
  403. *
  404. * @return array
  405. * The processed element.
  406. */
  407. public static function processGroup(&$element, FormStateInterface $form_state, &$complete_form) {
  408. $parents = implode('][', $element['#parents']);
  409. // Each details element forms a new group. The #type 'vertical_tabs' basically
  410. // only injects a new details element.
  411. $groups = &$form_state->getGroups();
  412. $groups[$parents]['#group_exists'] = TRUE;
  413. $element['#groups'] = &$groups;
  414. // Process vertical tabs group member details elements.
  415. if (isset($element['#group'])) {
  416. // Add this details element to the defined group (by reference).
  417. $group = $element['#group'];
  418. $groups[$group][] = &$element;
  419. }
  420. return $element;
  421. }
  422. }