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