Drupal 8.0.0 beta 12. More info: https://www.drupal.org/node/2514176

This commit is contained in:
Pantheon Automation 2015-08-17 17:00:26 -07:00 committed by Greg Anderson
commit 9921556621
13277 changed files with 1459781 additions and 0 deletions

View file

@ -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 {
}

View file

@ -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 {
}

View file

@ -0,0 +1,49 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\AttachmentsInterface.
*/
namespace Drupal\Core\Render;
/**
* Defines an interface for responses that can expose #attached metadata.
*
* @todo If in Drupal 9, we remove attachments other than assets (libraries +
* drupalSettings), then we can look into unifying this with
* \Drupal\Core\Asset\AttachedAssetsInterface.
*
* @see \Drupal\Core\Render\AttachmentsTrait
*/
interface AttachmentsInterface {
/**
* Gets attachments.
*
* @return array
* The attachments.
*/
public function getAttachments();
/**
* Adds attachments.
*
* @param array $attachments
* The attachments to add.
*
* @return $this
*/
public function addAttachments(array $attachments);
/**
* Sets attachments.
*
* @param array $attachments
* The attachments to set.
*
* @return $this
*/
public function setAttachments(array $attachments);
}

View file

@ -0,0 +1,30 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\AttachmentsResponseProcessorInterface.
*/
namespace Drupal\Core\Render;
/**
* Defines an interface for processing attachments of responses that have them.
*
* @see \Drupal\Core\Render\HtmlResponse
* @see \Drupal\Core\Render\HtmlResponseAttachmentsProcessor
*/
interface AttachmentsResponseProcessorInterface {
/**
* Processes the attachments of a response that has attachments.
*
* @param \Drupal\Core\Render\AttachmentsInterface $response
* The response to process the attachments for.
*
* @return \Drupal\Core\Render\AttachmentsInterface
* The processed response.
*
* @throws \InvalidArgumentException
*/
public function processAttachments(AttachmentsInterface $response);
}

View file

@ -0,0 +1,47 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\AttachmentsTrait.
*/
namespace Drupal\Core\Render;
/**
* Provides an implementation of AttachmentsInterface.
*
* @see \Drupal\Core\Render\AttachmentsInterface
*/
trait AttachmentsTrait {
/**
* The attachments for this response.
*
* @var array
*/
protected $attachments = [];
/**
* {@inheritdoc}
*/
public function getAttachments() {
return $this->attachments;
}
/**
* {@inheritdoc}
*/
public function addAttachments(array $attachments) {
$this->attachments = BubbleableMetadata::mergeAttachments($this->attachments, $attachments);
return $this;
}
/**
* {@inheritdoc}
*/
public function setAttachments(array $attachments) {
$this->attachments = $attachments;
return $this;
}
}

View file

@ -0,0 +1,81 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\BareHtmlPageRenderer.
*/
namespace Drupal\Core\Render;
/**
* Default bare HTML page renderer.
*/
class BareHtmlPageRenderer implements BareHtmlPageRendererInterface {
/**
* The renderer service.
*
* @var \Drupal\Core\Render\Renderer
*/
protected $renderer;
/**
* The HTML response attachments processor service.
*
* @var \Drupal\Core\Render\AttachmentsResponseProcessorInterface
*/
protected $htmlResponseAttachmentsProcessor;
/**
* Constructs a new BareHtmlPageRenderer.
*
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer service.
* @param \Drupal\Core\Render\AttachmentsResponseProcessorInterface $html_response_attachments_processor
* The HTML response attachments processor service.
*/
public function __construct(RendererInterface $renderer, AttachmentsResponseProcessorInterface $html_response_attachments_processor) {
$this->renderer = $renderer;
$this->htmlResponseAttachmentsProcessor = $html_response_attachments_processor;
}
/**
* {@inheritdoc}
*/
public function renderBarePage(array $content, $title, $page_theme_property, array $page_additions = []) {
$attributes = [
'class' => [
str_replace('_', '-', $page_theme_property),
],
];
$html = [
'#type' => 'html',
'#attributes' => $attributes,
'page' => [
'#type' => 'page',
'#theme' => $page_theme_property,
'#title' => $title,
'content' => $content,
] + $page_additions,
];
// For backwards compatibility.
// @todo In Drupal 9, add a $show_messages function parameter.
if (!isset($page_additions['#show_messages']) || $page_additions['#show_messages'] === TRUE) {
$html['page']['highlighted'] = ['#type' => 'status_messages'];
}
// Add the bare minimum of attachments from the system module and the
// current maintenance theme.
system_page_attachments($html['page']);
$this->renderer->renderRoot($html);
$response = new HtmlResponse();
$response->setContent($html);
// Process attachments, because this does not go via the regular render
// pipeline, but will be sent directly.
$response = $this->htmlResponseAttachmentsProcessor->processAttachments($response);
return $response;
}
}

View file

@ -0,0 +1,67 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\BareHtmlPageRendererInterface.
*/
namespace Drupal\Core\Render;
/**
* Bare HTML page renderer.
*
* By "bare HTML page", we mean that the following hooks that allow for "normal"
* pages are not invoked:
* - hook_page_attachments()
* - hook_page_attachments_alter()
* - hook_page_top()
* - hook_page_bottom()
*
* Examples of bare HTML pages are:
* - install.php
* - update.php
* - authorize.php
* - maintenance mode
* - exception handlers
*
* i.e. use this when rendering HTML pages in limited environments. Otherwise,
* use a @code _controller @endcode route, and return a render array.
* This will cause a main content renderer
* (\Drupal\Core\Render\MainContent\MainContentRendererInterface) to be
* used, and in case of a HTML request that will be
* \Drupal\Core\Render\MainContent\HtmlRenderer.
*
* In fact, this is not only *typically* used in a limited environment, it even
* *must* be used in a limited environment: when using the bare HTML page
* renderer, use as little state/additional services as possible, because the
* same safeguards aren't present (precisely because this is intended to be used
* in a limited environment).
*
* Currently, there are two types of bare pages available:
* - Install (hook_preprocess_install_page(), install-page.html.twig).
* - Maintenance (hook_preprocess_maintenance_page(),
* maintenance-page.html.twig).
*
* @see \Drupal\Core\Render\MainContent\HtmlRenderer
*/
interface BareHtmlPageRendererInterface {
/**
* Renders a bare page.
*
* @param array $content
* The main content to render in the 'content' region.
* @param string $title
* The title for this maintenance page.
* @param string $page_theme_property
* The #theme property to set on #type 'page'.
* @param array $page_additions
* Additional regions to add to the page. May also be used to pass the
* #show_messages property for #type 'page'.
*
* @return \Drupal\Core\Render\HtmlResponse
* The rendered HTML response, ready to be sent.
*/
public function renderBarePage(array $content, $title, $page_theme_property, array $page_additions = []);
}

View file

@ -0,0 +1,148 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\BubbleableMetadata.
*/
namespace Drupal\Core\Render;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\CacheableMetadata;
/**
* Value object used for bubbleable rendering metadata.
*
* @see \Drupal\Core\Render\RendererInterface::render()
*/
class BubbleableMetadata extends CacheableMetadata implements AttachmentsInterface {
use AttachmentsTrait;
/**
* Merges the values of another bubbleable metadata object with this one.
*
* @param \Drupal\Core\Cache\CacheableMetadata $other
* The other bubbleable metadata object.
*
* @return static
* A new bubbleable metadata object, with the merged data.
*/
public function merge(CacheableMetadata $other) {
$result = parent::merge($other);
// This is called many times per request, so avoid merging unless absolutely
// necessary.
if ($other instanceof BubbleableMetadata) {
if (empty($this->attachments)) {
$result->attachments = $other->attachments;
}
elseif (empty($other->attachments)) {
$result->attachments = $this->attachments;
}
else {
$result->attachments = static::mergeAttachments($this->attachments, $other->attachments);
}
}
return $result;
}
/**
* Applies the values of this bubbleable metadata object to a render array.
*
* @param array &$build
* A render array.
*/
public function applyTo(array &$build) {
parent::applyTo($build);
$build['#attached'] = $this->attachments;
}
/**
* Creates a bubbleable metadata object with values taken from a render array.
*
* @param array $build
* A render array.
*
* @return static
*/
public static function createFromRenderArray(array $build) {
$meta = parent::createFromRenderArray($build);
$meta->attachments = (isset($build['#attached'])) ? $build['#attached'] : [];
return $meta;
}
/**
* Merges two attachments arrays (which live under the '#attached' key).
*
* The values under the 'drupalSettings' key are merged in a special way, to
* match the behavior of:
*
* @code
* jQuery.extend(true, {}, $settings_items[0], $settings_items[1], ...)
* @endcode
*
* This means integer indices are preserved just like string indices are,
* rather than re-indexed as is common in PHP array merging.
*
* Example:
* @code
* function module1_page_attachments(&$page) {
* $page['a']['#attached']['drupalSettings']['foo'] = ['a', 'b', 'c'];
* }
* function module2_page_attachments(&$page) {
* $page['#attached']['drupalSettings']['foo'] = ['d'];
* }
* // When the page is rendered after the above code, and the browser runs the
* // resulting <SCRIPT> tags, the value of drupalSettings.foo is
* // ['d', 'b', 'c'], not ['a', 'b', 'c', 'd'].
* @endcode
*
* By following jQuery.extend() merge logic rather than common PHP array merge
* logic, the following are ensured:
* - Attaching JavaScript settings is idempotent: attaching the same settings
* twice does not change the output sent to the browser.
* - If pieces of the page are rendered in separate PHP requests and the
* returned settings are merged by JavaScript, the resulting settings are
* the same as if rendered in one PHP request and merged by PHP.
*
* @param array $a
* An attachments array.
* @param array $b
* Another attachments array.
*
* @return array
* The merged attachments array.
*/
public static function mergeAttachments(array $a, array $b) {
// If both #attached arrays contain drupalSettings, then merge them
// correctly; adding the same settings multiple times needs to behave
// idempotently.
if (!empty($a['drupalSettings']) && !empty($b['drupalSettings'])) {
$drupalSettings = NestedArray::mergeDeepArray(array($a['drupalSettings'], $b['drupalSettings']), TRUE);
// No need for re-merging them.
unset($a['drupalSettings']);
unset($b['drupalSettings']);
}
// Optimize merging of placeholders: no need for deep merging.
if (!empty($a['placeholders']) && !empty($b['placeholders'])) {
$placeholders = $a['placeholders'] + $b['placeholders'];
// No need for re-merging them.
unset($a['placeholders']);
unset($b['placeholders']);
}
// Apply the normal merge.
$a = array_merge_recursive($a, $b);
if (isset($drupalSettings)) {
// Save the custom merge for the drupalSettings.
$a['drupalSettings'] = $drupalSettings;
}
if (isset($placeholders)) {
// Save the custom merge for the placeholders.
$a['placeholders'] = $placeholders;
}
return $a;
}
}

View file

@ -0,0 +1,209 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element.
*/
namespace Drupal\Core\Render;
use Drupal\Component\Utility\SafeMarkup;
/**
* Provides helper methods for Drupal render elements.
*
* @see \Drupal\Core\Render\Element\ElementInterface
*
* @ingroup theme_render
*/
class Element {
/**
* Checks if the key is a property.
*
* @param string $key
* The key to check.
*
* @return bool
* TRUE of the key is a property, FALSE otherwise.
*/
public static function property($key) {
return $key[0] == '#';
}
/**
* Gets properties of a structured array element (keys beginning with '#').
*
* @param array $element
* An element array to return properties for.
*
* @return array
* An array of property keys for the element.
*/
public static function properties(array $element) {
return array_filter(array_keys($element), 'static::property');
}
/**
* Checks if the key is a child.
*
* @param string $key
* The key to check.
*
* @return bool
* TRUE if the element is a child, FALSE otherwise.
*/
public static function child($key) {
return !isset($key[0]) || $key[0] != '#';
}
/**
* Identifies the children of an element array, optionally sorted by weight.
*
* The children of a element array are those key/value pairs whose key does
* not start with a '#'. See drupal_render() for details.
*
* @param array $elements
* The element array whose children are to be identified. Passed by
* reference.
* @param bool $sort
* Boolean to indicate whether the children should be sorted by weight.
*
* @return array
* The array keys of the element's children.
*/
public static function children(array &$elements, $sort = FALSE) {
// Do not attempt to sort elements which have already been sorted.
$sort = isset($elements['#sorted']) ? !$elements['#sorted'] : $sort;
// Filter out properties from the element, leaving only children.
$count = count($elements);
$child_weights = array();
$i = 0;
$sortable = FALSE;
foreach ($elements as $key => $value) {
if ($key === '' || $key[0] !== '#') {
if (is_array($value)) {
if (isset($value['#weight'])) {
$weight = $value['#weight'];
$sortable = TRUE;
}
else {
$weight = 0;
}
// Supports weight with up to three digit precision and conserve
// the insertion order.
$child_weights[$key] = floor($weight * 1000) + $i / $count;
}
// Only trigger an error if the value is not null.
// @see https://www.drupal.org/node/1283892
elseif (isset($value)) {
trigger_error(SafeMarkup::format('"@key" is an invalid render array key', array('@key' => $key)), E_USER_ERROR);
}
}
$i++;
}
// Sort the children if necessary.
if ($sort && $sortable) {
asort($child_weights);
// Put the sorted children back into $elements in the correct order, to
// preserve sorting if the same element is passed through
// \Drupal\Core\Render\Element::children() twice.
foreach ($child_weights as $key => $weight) {
$value = $elements[$key];
unset($elements[$key]);
$elements[$key] = $value;
}
$elements['#sorted'] = TRUE;
}
return array_keys($child_weights);
}
/**
* Returns the visible children of an element.
*
* @param array $elements
* The parent element.
*
* @return array
* The array keys of the element's visible children.
*/
public static function getVisibleChildren(array $elements) {
$visible_children = array();
foreach (static::children($elements) as $key) {
$child = $elements[$key];
// Skip un-accessible children.
if (isset($child['#access']) && !$child['#access']) {
continue;
}
// Skip value and hidden elements, since they are not rendered.
if (!static::isVisibleElement($child)) {
continue;
}
$visible_children[$key] = $child;
}
return array_keys($visible_children);
}
/**
* Determines if an element is visible.
*
* @param array $element
* The element to check for visibility.
*
* @return bool
* TRUE if the element is visible, otherwise FALSE.
*/
public static function isVisibleElement($element) {
return (!isset($element['#type']) || !in_array($element['#type'], ['value', 'hidden', 'token'])) && (!isset($element['#access']) || $element['#access']);
}
/**
* Sets HTML attributes based on element properties.
*
* @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 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) {
// If the key is numeric, the attribute name needs to be taken over.
if (is_int($property)) {
$property = '#' . $attribute;
}
// Do not overwrite already existing attributes.
if (isset($element[$property]) && !isset($element['#attributes'][$attribute])) {
$element['#attributes'][$attribute] = $element[$property];
}
}
}
/**
* Indicates whether the given element is empty.
*
* An element that only has #cache set is considered empty, because it will
* render to the empty string.
*
* @param array $elements
* The element.
*
* @return bool
* Whether the given element is empty.
*/
public static function isEmpty(array $elements) {
return empty($elements) || (count($elements) === 1 && array_keys($elements) === ['#cache']);
}
}

View file

@ -0,0 +1,108 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\Actions.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
/**
* Provides a wrapper element to group one or more buttons in a form.
*
* Use of the 'actions' element as an array key helps to ensure proper styling
* in themes and to enable other modules to properly alter a form's actions.
*
* @RenderElement("actions")
*/
class Actions extends Container {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return array(
'#process' => array(
// @todo Move this to #pre_render.
array($class, 'preRenderActionsDropbutton'),
array($class, 'processActions'),
array($class, 'processContainer'),
),
'#weight' => 100,
'#theme_wrappers' => array('container'),
);
}
/**
* Processes a form actions container element.
*
* @param array $element
* An associative array containing the properties and children of the
* form actions container.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param array $complete_form
* The complete form structure.
*
* @return array
* The processed element.
*/
public static function processActions(&$element, FormStateInterface $form_state, &$complete_form) {
$element['#attributes']['class'][] = 'form-actions';
return $element;
}
/**
* #pre_render callback for #type 'actions'.
*
* This callback iterates over all child elements of the #type 'actions'
* container to look for elements with a #dropbutton property, so as to group
* those elements into dropbuttons. As such, it works similar to #group, but is
* specialized for dropbuttons.
*
* The value of #dropbutton denotes the dropbutton to group the child element
* into. For example, two different values of 'foo' and 'bar' on child elements
* would generate two separate dropbuttons, which each contain the corresponding
* buttons.
*
* @param array $element
* The #type 'actions' element to process.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param array $complete_form
* The complete form structure.
*
* @return array
* The processed #type 'actions' element, including individual buttons grouped
* into new #type 'dropbutton' elements.
*/
public static function preRenderActionsDropbutton(&$element, FormStateInterface $form_state, &$complete_form) {
$dropbuttons = array();
foreach (Element::children($element, TRUE) as $key) {
if (isset($element[$key]['#dropbutton'])) {
$dropbutton = $element[$key]['#dropbutton'];
// If there is no dropbutton for this button group yet, create one.
if (!isset($dropbuttons[$dropbutton])) {
$dropbuttons[$dropbutton] = array(
'#type' => 'dropbutton',
);
}
// Add this button to the corresponding dropbutton.
// @todo Change #type 'dropbutton' to be based on item-list.html.twig
// instead of links.html.twig to avoid this preemptive rendering.
$button = \Drupal::service('renderer')->renderPlain($element[$key]);
$dropbuttons[$dropbutton]['#links'][$key] = array(
'title' => $button,
);
}
}
// @todo For now, all dropbuttons appear first. Consider to invent a more
// fancy sorting/injection algorithm here.
return $dropbuttons + $element;
}
}

View file

@ -0,0 +1,36 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\Ajax.
*/
namespace Drupal\Core\Render\Element;
/**
* Provides a render element for adding Ajax to a render element.
*
* Holds an array whose values control the Ajax behavior of the element.
*
* @ingroup ajax
*
* @RenderElement("ajax")
*/
class Ajax extends RenderElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
// By default, we don't want Ajax commands being rendered in the context of
// an HTML page, so we don't provide defaults for #theme or #theme_wrappers.
// However, modules can set these properties (for example, to provide an
// HTML debugging page that displays rather than executes Ajax commands).
return array(
'#header' => TRUE,
'#commands' => array(),
'#error' => NULL,
);
}
}

View file

@ -0,0 +1,89 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\Button.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
/**
* Provides an action button form element.
*
* When the button is pressed, the form will be submitted to Drupal, where it is
* validated and rebuilt. The submit handler is not invoked.
*
* @FormElement("button")
*/
class Button extends FormElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return array(
'#input' => TRUE,
'#name' => 'op',
'#is_button' => TRUE,
'#executes_submit_callback' => FALSE,
'#limit_validation_errors' => FALSE,
'#process' => array(
array($class, 'processButton'),
array($class, 'processAjaxForm'),
),
'#pre_render' => array(
array($class, 'preRenderButton'),
),
'#theme_wrappers' => array('input__submit'),
);
}
/**
* Processes a form button element.
*/
public static function processButton(&$element, FormStateInterface $form_state, &$complete_form) {
// If this is a button intentionally allowing incomplete form submission
// (e.g., a "Previous" or "Add another item" button), then also skip
// client-side validation.
if (isset($element['#limit_validation_errors']) && $element['#limit_validation_errors'] !== FALSE) {
$element['#attributes']['formnovalidate'] = 'formnovalidate';
}
return $element;
}
/**
* Prepares a #type 'button' render element for input.html.twig.
*
* @param array $element
* An associative array containing the properties of the element.
* Properties used: #attributes, #button_type, #name, #value.
*
* The #button_type property accepts any value, though core themes have CSS that
* styles the following button_types appropriately: 'primary', 'danger'.
*
* @return array
* The $element with prepared variables ready for input.html.twig.
*/
public static function preRenderButton($element) {
$element['#attributes']['type'] = 'submit';
Element::setAttributes($element, array('id', 'name', 'value'));
$element['#attributes']['class'][] = 'button';
if (!empty($element['#button_type'])) {
$element['#attributes']['class'][] = 'button--' . $element['#button_type'];
}
$element['#attributes']['class'][] = 'js-form-submit';
$element['#attributes']['class'][] = 'form-submit';
if (!empty($element['#attributes']['disabled'])) {
$element['#attributes']['class'][] = 'is-disabled';
}
return $element;
}
}

View file

@ -0,0 +1,139 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\Checkbox.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
/**
* Provides a form element for a single checkbox.
*
* Properties:
* - #return_value: The value to return when the checkbox is checked.
*
* Usage example:
* @code
* $form['copy'] = array(
* '#type' => 'checkbox',
* '#title' => t('Send me a copy'),
* );
* @endcode
*
* @see \Drupal\Core\Render\Element\Checkboxes
*
* @FormElement("checkbox")
*/
class Checkbox extends FormElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return array(
'#input' => TRUE,
'#return_value' => 1,
'#process' => array(
array($class, 'processCheckbox'),
array($class, 'processAjaxForm'),
array($class, 'processGroup'),
),
'#pre_render' => array(
array($class, 'preRenderCheckbox'),
array($class, 'preRenderGroup'),
),
'#theme' => 'input__checkbox',
'#theme_wrappers' => array('form_element'),
'#title_display' => 'after',
);
}
/**
* {@inheritdoc}
*/
public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
if ($input === FALSE) {
// Use #default_value as the default value of a checkbox, except change
// NULL to 0, because FormBuilder::handleInputElement() would otherwise
// replace NULL with empty string, but an empty string is a potentially
// valid value for a checked checkbox.
return isset($element['#default_value']) ? $element['#default_value'] : 0;
}
else {
// Checked checkboxes are submitted with a value (possibly '0' or ''):
// http://www.w3.org/TR/html401/interact/forms.html#successful-controls.
// For checked checkboxes, browsers submit the string version of
// #return_value, but we return the original #return_value. For unchecked
// checkboxes, browsers submit nothing at all, but
// FormBuilder::handleInputElement() detects this, and calls this
// function with $input=NULL. Returning NULL from a value callback means
// to use the default value, which is not what is wanted when an unchecked
// checkbox is submitted, so we use integer 0 as the value indicating an
// unchecked checkbox. Therefore, modules must not use integer 0 as a
// #return_value, as doing so results in the checkbox always being treated
// as unchecked. The string '0' is allowed for #return_value. The most
// common use-case for setting #return_value to either 0 or '0' is for the
// first option within a 0-indexed array of checkboxes, and for this,
// \Drupal\Core\Render\Element\Checkboxes::processCheckboxes() uses the
// string rather than the integer.
return isset($input) ? $element['#return_value'] : 0;
}
}
/**
* Prepares a #type 'checkbox' render element for input.html.twig.
*
* @param array $element
* An associative array containing the properties of the element.
* Properties used: #title, #value, #return_value, #description, #required,
* #attributes, #checked.
*
* @return array
* The $element with prepared variables ready for input.html.twig.
*/
public static function preRenderCheckbox($element) {
$element['#attributes']['type'] = 'checkbox';
Element::setAttributes($element, array('id', 'name', '#return_value' => 'value'));
// Unchecked checkbox has #value of integer 0.
if (!empty($element['#checked'])) {
$element['#attributes']['checked'] = 'checked';
}
static::setAttributes($element, array('form-checkbox'));
return $element;
}
/**
* Sets the #checked property of a checkbox element.
*/
public static function processCheckbox(&$element, FormStateInterface $form_state, &$complete_form) {
$value = $element['#value'];
$return_value = $element['#return_value'];
// On form submission, the #value of an available and enabled checked
// checkbox is #return_value, and the #value of an available and enabled
// unchecked checkbox is integer 0. On not submitted forms, and for
// checkboxes with #access=FALSE or #disabled=TRUE, the #value is
// #default_value (integer 0 if #default_value is NULL). Most of the time,
// a string comparison of #value and #return_value is sufficient for
// determining the "checked" state, but a value of TRUE always means checked
// (even if #return_value is 'foo'), and a value of FALSE or integer 0
// always means unchecked (even if #return_value is '' or '0').
if ($value === TRUE || $value === FALSE || $value === 0) {
$element['#checked'] = (bool) $value;
}
else {
// Compare as strings, so that 15 is not considered equal to '15foo', but
// 1 is considered equal to '1'. This cast does not imply that either
// #value or #return_value is expected to be a string.
$element['#checked'] = ((string) $value === (string) $return_value);
}
return $element;
}
}

View file

@ -0,0 +1,130 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\Checkboxes.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides a form element for a set of checkboxes.
*
* Properties:
* - #options: An associative array whose keys are the values returned for each
* checkbox, and whose values are the labels next to each checkbox. The
* #options array cannot have a 0 key, as it would not be possible to discern
* checked and unchecked states.
*
* Usage example:
* @code
* $form['high_school']['tests_taken'] = array(
* '#type' => 'checkboxes',
* '#options' => array('SAT' => t('SAT'), 'ACT' => t('ACT'))),
* '#title' => t('What standardized tests did you take?'),
* ...
* );
* @endcode
*
* @see \Drupal\Core\Render\Element\Radios
* @see \Drupal\Core\Render\Element\Checkbox
*
* @FormElement("checkboxes")
*/
class Checkboxes extends FormElement {
use CompositeFormElementTrait;
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return array(
'#input' => TRUE,
'#process' => array(
array($class, 'processCheckboxes'),
),
'#pre_render' => array(
array($class, 'preRenderCompositeFormElement'),
),
'#theme_wrappers' => array('checkboxes'),
);
}
/**
* Processes a checkboxes form element.
*/
public static function processCheckboxes(&$element, FormStateInterface $form_state, &$complete_form) {
$value = is_array($element['#value']) ? $element['#value'] : array();
$element['#tree'] = TRUE;
if (count($element['#options']) > 0) {
if (!isset($element['#default_value']) || $element['#default_value'] == 0) {
$element['#default_value'] = array();
}
$weight = 0;
foreach ($element['#options'] as $key => $choice) {
// Integer 0 is not a valid #return_value, so use '0' instead.
// @see \Drupal\Core\Render\Element\Checkbox::valueCallback().
// @todo For Drupal 8, cast all integer keys to strings for consistency
// with \Drupal\Core\Render\Element\Radios::processRadios().
if ($key === 0) {
$key = '0';
}
// Maintain order of options as defined in #options, in case the element
// defines custom option sub-elements, but does not define all option
// sub-elements.
$weight += 0.001;
$element += array($key => array());
$element[$key] += array(
'#type' => 'checkbox',
'#title' => $choice,
'#return_value' => $key,
'#default_value' => isset($value[$key]) ? $key : NULL,
'#attributes' => $element['#attributes'],
'#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL,
// Errors should only be shown on the parent checkboxes element.
'#error_no_message' => TRUE,
'#weight' => $weight,
);
}
}
return $element;
}
/**
* {@inheritdoc}
*/
public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
if ($input === FALSE) {
$value = array();
$element += array('#default_value' => array());
foreach ($element['#default_value'] as $key) {
$value[$key] = $key;
}
return $value;
}
elseif (is_array($input)) {
// Programmatic form submissions use NULL to indicate that a checkbox
// should be unchecked. We therefore remove all NULL elements from the
// array before constructing the return value, to simulate the behavior
// of web browsers (which do not send unchecked checkboxes to the server
// at all). This will not affect non-programmatic form submissions, since
// all values in \Drupal::request()->request are strings.
// @see \Drupal\Core\Form\FormBuilderInterface::submitForm()
foreach ($input as $key => $value) {
if (!isset($value)) {
unset($input[$key]);
}
}
return array_combine($input, $input);
}
else {
return array();
}
}
}

View file

@ -0,0 +1,82 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\Color.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Component\Utility\Color as ColorUtility;
/**
* Provides a form element for choosing a color.
*
* @FormElement("color")
*/
class Color extends FormElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return array(
'#input' => TRUE,
'#process' => array(
array($class, 'processAjaxForm'),
),
'#element_validate' => array(
array($class, 'validateColor'),
),
'#pre_render' => array(
array($class, 'preRenderColor'),
),
'#theme' => 'input__color',
'#theme_wrappers' => array('form_element'),
);
}
/**
* Form element validation handler for #type 'color'.
*/
public static function validateColor(&$element, FormStateInterface $form_state, &$complete_form) {
$value = trim($element['#value']);
// Default to black if no value is given.
// @see http://www.w3.org/TR/html5/number-state.html#color-state
if ($value === '') {
$form_state->setValueForElement($element, '#000000');
}
else {
// Try to parse the value and normalize it.
try {
$form_state->setValueForElement($element, ColorUtility::rgbToHex(ColorUtility::hexToRgb($value)));
}
catch (\InvalidArgumentException $e) {
$form_state->setError($element, t('%name must be a valid color.', array('%name' => empty($element['#title']) ? $element['#parents'][0] : $element['#title'])));
}
}
}
/**
* Prepares a #type 'color' render element for input.html.twig.
*
* @param array $element
* An associative array containing the properties of the element.
* Properties used: #title, #value, #description, #attributes.
*
* @return array
* The $element with prepared variables ready for input.html.twig.
*/
public static function preRenderColor($element) {
$element['#attributes']['type'] = 'color';
Element::setAttributes($element, array('id', 'name', 'value'));
static::setAttributes($element, array('form-color'));
return $element;
}
}

View file

@ -0,0 +1,43 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\CompositeFormElementTrait.
*/
namespace Drupal\Core\Render\Element;
/**
* Provides a trait for radios, checkboxes, and similar composite form elements.
*
* Any form element that is comprised of several distinct parts can use this
* trait to add support for a composite title or description.
*/
trait CompositeFormElementTrait {
/**
* Adds form element theming to an element if its title or description is set.
*
* This is used as a pre render function for checkboxes and radios.
*/
public static function preRenderCompositeFormElement($element) {
// Set the element's title attribute to show #title as a tooltip, if needed.
if (isset($element['#title']) && $element['#title_display'] == 'attribute') {
$element['#attributes']['title'] = $element['#title'];
if (!empty($element['#required'])) {
// Append an indication that this field is required.
$element['#attributes']['title'] .= ' (' . t('Required') . ')';
}
}
if (isset($element['#title']) || isset($element['#description'])) {
// @see #type 'fieldgroup'
$element['#attributes']['id'] = $element['#id'] . '--wrapper';
$element['#theme_wrappers'][] = 'fieldset';
$element['#attributes']['class'][] = 'fieldgroup';
$element['#attributes']['class'][] = 'form-composite';
}
return $element;
}
}

View file

@ -0,0 +1,62 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\Container.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Component\Utility\Html as HtmlUtility;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides a render element that wraps child elements in a container.
*
* Surrounds child elements with a <div> and adds attributes such as classes or
* an HTML ID.
*
* @RenderElement("container")
*/
class Container extends RenderElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return array(
'#process' => array(
array($class, 'processGroup'),
array($class, 'processContainer'),
),
'#pre_render' => array(
array($class, 'preRenderGroup'),
),
'#theme_wrappers' => array('container'),
);
}
/**
* Processes a container element.
*
* @param array $element
* An associative array containing the properties and children of the
* container.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param array $complete_form
* The complete form structure.
*
* @return array
* The processed element.
*/
public static function processContainer(&$element, FormStateInterface $form_state, &$complete_form) {
// Generate the ID of the element if it's not explicitly given.
if (!isset($element['#id'])) {
$element['#id'] = HtmlUtility::getUniqueId(implode('-', $element['#parents']) . '-wrapper');
}
return $element;
}
}

View file

@ -0,0 +1,68 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\Date.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Core\Render\Element;
/**
* Provides a form element for date selection.
*
* The #default_value will be today's date if no value is supplied. The format
* for the #default_value and the #return_value is an array with three elements
* with the keys: 'year', month', and 'day'. For example,
* array('year' => 2007, 'month' => 2, 'day' => 15)
*
* @FormElement("date")
*/
class Date extends FormElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return array(
'#input' => TRUE,
'#theme' => 'input__date',
'#pre_render' => array(
array($class, 'preRenderDate'),
),
'#theme_wrappers' => array('form_element'),
);
}
/**
* Adds form-specific attributes to a 'date' #type element.
*
* Supports HTML5 types of 'date', 'datetime', 'datetime-local', and 'time'.
* Falls back to a plain textfield. Used as a sub-element by the datetime
* element type.
*
* @param array $element
* An associative array containing the properties of the element.
* Properties used: #title, #value, #options, #description, #required,
* #attributes, #id, #name, #type, #min, #max, #step, #value, #size.
*
* Note: The input "name" attribute needs to be sanitized before output, which
* is currently done by initializing Drupal\Core\Template\Attribute with
* all the attributes.
*
* @return array
* The $element with prepared variables ready for #theme 'input__date'.
*/
public static function preRenderDate($element) {
if (empty($element['#attributes']['type'])) {
$element['#attributes']['type'] = 'date';
}
Element::setAttributes($element, array('id', 'name', 'type', 'min', 'max', 'step', 'value', 'size'));
static::setAttributes($element, array('form-' . $element['#attributes']['type']));
return $element;
}
}

View file

@ -0,0 +1,78 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\Details.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Core\Render\Element;
/**
* Provides a render element for a details element, similar to a fieldset.
*
* Fieldsets can only be used in forms, while details elements can be used
* outside of forms.
*
* @see \Drupal\Core\Render\Element\Fieldset
*
* @RenderElement("details")
*/
class Details extends RenderElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return array(
'#open' => FALSE,
'#value' => NULL,
'#process' => array(
array($class, 'processGroup'),
array($class, 'processAjaxForm'),
),
'#pre_render' => array(
array($class, 'preRenderDetails'),
array($class, 'preRenderGroup'),
),
'#theme_wrappers' => array('details'),
);
}
/**
* Adds form element theming to details.
*
* @param $element
* An associative array containing the properties and children of the
* details.
*
* @return
* The modified element.
*/
public static function preRenderDetails($element) {
Element::setAttributes($element, array('id'));
// The .js-form-wrapper class is required for #states to treat details like
// containers.
static::setAttributes($element, array('js-form-wrapper', 'form-wrapper'));
// Collapsible details.
$element['#attached']['library'][] = 'core/drupal.collapse';
if (!empty($element['#open'])) {
$element['#attributes']['open'] = 'open';
}
// Do not render optional details elements if there are no children.
if (isset($element['#parents'])) {
$group = implode('][', $element['#parents']);
if (!empty($element['#optional']) && !Element::getVisibleChildren($element['#groups'][$group])) {
$element['#printed'] = TRUE;
}
}
return $element;
}
}

View file

@ -0,0 +1,52 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\Dropbutton.
*/
namespace Drupal\Core\Render\Element;
/**
* Provides a render element for a set of links rendered as a drop-down button.
*
* @see \Drupal\Core\Render\Element\Operations
*
* @RenderElement("dropbutton")
*/
class Dropbutton extends RenderElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return array(
'#pre_render' => array(
array($class, 'preRenderDropbutton'),
),
'#theme' => 'links__dropbutton',
);
}
/**
* Pre-render callback: Attaches the dropbutton library and required markup.
*/
public static function preRenderDropbutton($element) {
$element['#attached']['library'][] = 'core/drupal.dropbutton';
$element['#attributes']['class'][] = 'dropbutton';
if (!isset($element['#theme_wrappers'])) {
$element['#theme_wrappers'] = array();
}
array_unshift($element['#theme_wrappers'], 'dropbutton_wrapper');
// Enable targeted theming of specific dropbuttons (e.g., 'operations' or
// 'operations__node').
if (isset($element['#subtype'])) {
$element['#theme'] .= '__' . $element['#subtype'];
}
return $element;
}
}

View file

@ -0,0 +1,57 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\ElementInterface.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Component\Plugin\PluginInspectionInterface;
/**
* Provides an interface for render element plugins.
*
* Render element plugins allow modules to declare their own Render API element
* types and specify the default values for the properties. The values returned
* 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();
/**
* Sets a form element's class attribute.
*
* Adds 'required' and 'error' classes as needed.
*
* @param array $element
* The form element.
* @param array $class
* Array of new class names to be added.
*/
public static function setAttributes(&$element, $class = array());
}

View file

@ -0,0 +1,90 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\Email.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
/**
* Provides a form input element for entering an email address.
*
* @FormElement("email")
*/
class Email extends FormElement {
/**
* Defines the max length for an email address
*
* The maximum length of an email address is 254 characters. RFC 3696
* specifies a total length of 320 characters, but mentions that
* addresses longer than 256 characters are not normally useful. Erratum
* 1690 was then released which corrected this value to 254 characters.
* @see http://tools.ietf.org/html/rfc3696#section-3
* @see http://www.rfc-editor.org/errata_search.php?rfc=3696&eid=1690
*/
const EMAIL_MAX_LENGTH = 254;
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return array(
'#input' => TRUE,
'#size' => 60,
'#maxlength' => self::EMAIL_MAX_LENGTH,
'#autocomplete_route_name' => FALSE,
'#process' => array(
array($class, 'processAutocomplete'),
array($class, 'processAjaxForm'),
array($class, 'processPattern'),
),
'#element_validate' => array(
array($class, 'validateEmail'),
),
'#pre_render' => array(
array($class, 'preRenderEmail'),
),
'#theme' => 'input__email',
'#theme_wrappers' => array('form_element'),
);
}
/**
* Form element validation handler for #type 'email'.
*
* Note that #maxlength and #required is validated by _form_validate() already.
*/
public static function validateEmail(&$element, FormStateInterface $form_state, &$complete_form) {
$value = trim($element['#value']);
$form_state->setValueForElement($element, $value);
if ($value !== '' && !\Drupal::service('email.validator')->isValid($value)) {
$form_state->setError($element, t('The email address %mail is not valid.', array('%mail' => $value)));
}
}
/**
* Prepares a #type 'email' render element for input.html.twig.
*
* @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 input.html.twig.
*/
public static function preRenderEmail($element) {
$element['#attributes']['type'] = 'email';
Element::setAttributes($element, array('id', 'name', 'value', 'size', 'maxlength', 'placeholder'));
static::setAttributes($element, array('form-email'));
return $element;
}
}

View file

@ -0,0 +1,29 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\Fieldgroup.
*/
namespace Drupal\Core\Render\Element;
/**
* Provides a render element for a group of form elements.
*
* In default rendering, the only difference between a 'fieldgroup' and a
* 'fieldset' is the CSS class applied to the containing HTML element.
*
* @see \Drupal\Core\Render\Element\Fieldset
* @see \Drupal\Core\Render\Element\Details
*
* @RenderElement("fieldgroup")
*/
class Fieldgroup extends Fieldset {
public function getInfo() {
return array(
'#attributes' => array('class' => array('fieldgroup')),
) + parent::getInfo();
}
}

View file

@ -0,0 +1,43 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\Fieldset.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Core\Render\Element;
/**
* Provides a render element for a group of form elements.
*
* In default rendering, the only difference between a 'fieldgroup' and a
* 'fieldset' is the CSS class applied to the containing HTML element.
*
* @see \Drupal\Core\Render\Element\Fieldgroup
* @see \Drupal\Core\Render\Element\Details
*
* @RenderElement("fieldset")
*/
class Fieldset extends RenderElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return array(
'#process' => array(
array($class, 'processGroup'),
array($class, 'processAjaxForm'),
),
'#pre_render' => array(
array($class, 'preRenderGroup'),
),
'#value' => NULL,
'#theme_wrappers' => array('fieldset'),
);
}
}

View file

@ -0,0 +1,73 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\File.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
/**
* Provides a form element for uploading a file.
*
* @FormElement("file")
*/
class File extends FormElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return array(
'#input' => TRUE,
'#multiple' => FALSE,
'#process' => array(
array($class, 'processFile'),
),
'#size' => 60,
'#pre_render' => array(
array($class, 'preRenderFile'),
),
'#theme' => 'input__file',
'#theme_wrappers' => array('form_element'),
);
}
/**
* Processes a file upload element, make use of #multiple if present.
*/
public static function processFile(&$element, FormStateInterface $form_state, &$complete_form) {
if ($element['#multiple']) {
$element['#attributes'] = array('multiple' => 'multiple');
$element['#name'] .= '[]';
}
return $element;
}
/**
* Prepares a #type 'file' render element for input.html.twig.
*
* For assistance with handling the uploaded file correctly, see the API
* provided by file.inc.
*
* @param array $element
* An associative array containing the properties of the element.
* Properties used: #title, #name, #size, #description, #required,
* #attributes.
*
* @return array
* The $element with prepared variables ready for input.html.twig.
*/
public static function preRenderFile($element) {
$element['#attributes']['type'] = 'file';
Element::setAttributes($element, array('id', 'name', 'size'));
static::setAttributes($element, array('js-form-file', 'form-file'));
return $element;
}
}

View file

@ -0,0 +1,29 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\Form.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Core\Render\Element;
/**
* Provides a render element for a form.
*
* @RenderElement("form")
*/
class Form extends RenderElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
return array(
'#method' => 'post',
'#theme_wrappers' => array('form'),
);
}
}

View file

@ -0,0 +1,131 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\FormElement.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides a base class for form element 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;
}
/**
* #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.
*/
public static function processPattern(&$element, FormStateInterface $form_state, &$complete_form) {
if (isset($element['#pattern']) && !isset($element['#attributes']['pattern'])) {
$element['#attributes']['pattern'] = $element['#pattern'];
$element['#element_validate'][] = array(get_called_class(), 'validatePattern');
}
return $element;
}
/**
* #element_validate callback for #pattern form element property.
*
* @param $element
* An associative array containing the properties and children of the
* generic form element.
* @param $form_state
* The current state of the form.
* @param array $complete_form
* The complete form structure.
*/
public static function validatePattern(&$element, FormStateInterface $form_state, &$complete_form) {
if ($element['#value'] !== '') {
// The pattern must match the entire string and should have the same
// behavior as the RegExp object in ECMA 262.
// - Use bracket-style delimiters to avoid introducing a special delimiter
// character like '/' that would have to be escaped.
// - Put in brackets so that the pattern can't interfere with what's
// prepended and appended.
$pattern = '{^(?:' . $element['#pattern'] . ')$}';
if (!preg_match($pattern, $element['#value'])) {
$form_state->setError($element, t('%name field is not in the right format.', array('%name' => $element['#title'])));
}
}
}
/**
* 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;
}
}

View file

@ -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);
}

View file

@ -0,0 +1,55 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\Hidden.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Core\Render\Element;
/**
* Provides a form element for an HTML 'hidden' input element.
*
* @see \Drupal\Core\Render\Element\Value
*
* @FormElement("hidden")
*/
class Hidden extends FormElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return array(
'#input' => TRUE,
'#process' => array(
array($class, 'processAjaxForm'),
),
'#pre_render' => array(
array($class, 'preRenderHidden'),
),
'#theme' => 'input__hidden',
);
}
/**
* Prepares a #type 'hidden' render element for input.html.twig.
*
* @param array $element
* An associative array containing the properties of the element.
* Properties used: #name, #value, #attributes.
*
* @return array
* The $element with prepared variables ready for input.html.twig.
*/
public static function preRenderHidden($element) {
$element['#attributes']['type'] = 'hidden';
Element::setAttributes($element, array('name', 'value'));
return $element;
}
}

View file

@ -0,0 +1,30 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\Html.
*/
namespace Drupal\Core\Render\Element;
/**
* Provides a render element for an entire HTML page: <html> plus its children.
*
* @RenderElement("html")
*/
class Html extends RenderElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
return array(
'#theme' => 'html',
// HTML5 Shiv
'#attached' => array(
'library' => array('core/html5shiv'),
),
);
}
}

View file

@ -0,0 +1,195 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\HtmlTag.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Core\Template\Attribute;
/**
* Provides a render element for any HTML tag, with properties and value.
*
* @RenderElement("html_tag")
*/
class HtmlTag extends RenderElement {
/**
* Void elements do not contain values or closing tags.
* @see http://www.w3.org/TR/html5/syntax.html#syntax-start-tag
* @see http://www.w3.org/TR/html5/syntax.html#void-elements
*/
static protected $voidElements = array(
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr',
);
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return array(
'#pre_render' => array(
array($class, 'preRenderConditionalComments'),
array($class, 'preRenderHtmlTag'),
),
'#attributes' => array(),
'#value' => NULL,
);
}
/**
* Pre-render callback: Renders a generic HTML tag with attributes into #markup.
*
* Note: It is the caller's responsibility to sanitize any input parameters.
* This callback does not perform sanitization. Despite the result of this
* pre-render callback being a #markup element, it is not passed through
* \Drupal\Component\Utility\Xss::filterAdmin(). This is because it is marked
* safe here, which causes
* \Drupal\Component\Utility\SafeMarkup::checkAdminXss() to regard it as safe
* and bypass the call to \Drupal\Component\Utility\Xss::filterAdmin().
*
* @param array $element
* An associative array containing:
* - #tag: The tag name to output. Typical tags added to the HTML HEAD:
* - meta: To provide meta information, such as a page refresh.
* - link: To refer to stylesheets and other contextual information.
* - script: To load JavaScript.
* The value of #tag is not escaped or sanitized, so do not pass in user
* input.
* - #attributes: (optional) An array of HTML attributes to apply to the
* tag.
* - #value: (optional) A string containing tag content, such as inline
* CSS.
* - #value_prefix: (optional) A string to prepend to #value, e.g. a CDATA
* wrapper prefix.
* - #value_suffix: (optional) A string to append to #value, e.g. a CDATA
* wrapper suffix.
* - #noscript: (optional) If TRUE, the markup (including any prefix or
* suffix) will be wrapped in a <noscript> element. (Note that passing
* any non-empty value here will add the <noscript> tag.)
*
* @return array
*/
public static function preRenderHtmlTag($element) {
$attributes = isset($element['#attributes']) ? new Attribute($element['#attributes']) : '';
// Construct a void element.
if (in_array($element['#tag'], self::$voidElements)) {
// This function is intended for internal use, so we assume that no unsafe
// values are passed in #tag. The attributes are already safe because
// Attribute output is already automatically sanitized.
// @todo Escape this properly instead? https://www.drupal.org/node/2296101
$markup = SafeMarkup::set('<' . $element['#tag'] . $attributes . " />\n");
}
// Construct all other elements.
else {
$markup = '<' . $element['#tag'] . $attributes . '>';
if (isset($element['#value_prefix'])) {
$markup .= $element['#value_prefix'];
}
$markup .= $element['#value'];
if (isset($element['#value_suffix'])) {
$markup .= $element['#value_suffix'];
}
$markup .= '</' . $element['#tag'] . ">\n";
// @todo We cannot actually guarantee this markup is safe. Consider a fix
// in: https://www.drupal.org/node/2296101
$markup = SafeMarkup::set($markup);
}
if (!empty($element['#noscript'])) {
$element['#markup'] = SafeMarkup::format('<noscript>@markup</noscript>', ['@markup' => $markup]);
}
else {
$element['#markup'] = $markup;
}
return $element;
}
/**
* Pre-render callback: Renders #browsers into #prefix and #suffix.
*
* @param array $element
* A render array with a '#browsers' property. The '#browsers' property can
* contain any or all of the following keys:
* - 'IE': If FALSE, the element is not rendered by Internet Explorer. If
* TRUE, the element is rendered by Internet Explorer. Can also be a string
* containing an expression for Internet Explorer to evaluate as part of a
* conditional comment. For example, this can be set to 'lt IE 7' for the
* element to be rendered in Internet Explorer 6, but not in Internet
* Explorer 7 or higher. Defaults to TRUE.
* - '!IE': If FALSE, the element is not rendered by browsers other than
* Internet Explorer. If TRUE, the element is rendered by those browsers.
* Defaults to TRUE.
* Examples:
* - To render an element in all browsers, '#browsers' can be left out or set
* to array('IE' => TRUE, '!IE' => TRUE).
* - To render an element in Internet Explorer only, '#browsers' can be set
* to array('!IE' => FALSE).
* - To render an element in Internet Explorer 6 only, '#browsers' can be set
* to array('IE' => 'lt IE 7', '!IE' => FALSE).
* - To render an element in Internet Explorer 8 and higher and in all other
* browsers, '#browsers' can be set to array('IE' => 'gte IE 8').
*
* @return array
* The passed-in element with markup for conditional comments potentially
* added to '#prefix' and '#suffix'.
*/
public static function preRenderConditionalComments($element) {
$browsers = isset($element['#browsers']) ? $element['#browsers'] : array();
$browsers += array(
'IE' => TRUE,
'!IE' => TRUE,
);
// If rendering in all browsers, no need for conditional comments.
if ($browsers['IE'] === TRUE && $browsers['!IE']) {
return $element;
}
// Determine the conditional comment expression for Internet Explorer to
// evaluate.
if ($browsers['IE'] === TRUE) {
$expression = 'IE';
}
elseif ($browsers['IE'] === FALSE) {
$expression = '!IE';
}
else {
// The IE expression might contain some user input data.
$expression = SafeMarkup::checkAdminXss($browsers['IE']);
}
// If the #prefix and #suffix properties are used, wrap them with
// conditional comment markup. The conditional comment expression is
// evaluated by Internet Explorer only. To control the rendering by other
// browsers, use either the "downlevel-hidden" or "downlevel-revealed"
// technique. See http://en.wikipedia.org/wiki/Conditional_comment
// for details.
// Ensure what we are dealing with is safe.
// This would be done later anyway in drupal_render().
$prefix = isset($elements['#prefix']) ? SafeMarkup::checkAdminXss($elements['#prefix']) : '';
$suffix = isset($elements['#suffix']) ? SafeMarkup::checkAdminXss($elements['#suffix']) : '';
// Now calling SafeMarkup::set is safe, because we ensured the
// data coming in was at least admin escaped.
if (!$browsers['!IE']) {
// "downlevel-hidden".
$element['#prefix'] = SafeMarkup::set("\n<!--[if $expression]>\n" . $prefix);
$element['#suffix'] = SafeMarkup::set($suffix . "<![endif]-->\n");
}
else {
// "downlevel-revealed".
$element['#prefix'] = SafeMarkup::set("\n<!--[if $expression]><!-->\n" . $prefix);
$element['#suffix'] = SafeMarkup::set($suffix . "<!--<![endif]-->\n");
}
return $element;
}
}

View file

@ -0,0 +1,98 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\ImageButton.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
/**
* Provides a form element for a submit button with an image.
*
* @FormElement("image_button")
*/
class ImageButton extends Submit {
/**
* {@inheritdoc}
*/
public function getInfo() {
$info = parent::getInfo();
unset($info['name']);
return array(
'#return_value' => TRUE,
'#has_garbage_value' => TRUE,
'#src' => NULL,
'#theme_wrappers' => array('input__image_button'),
) + $info;
}
/**
* {@inheritdoc}
*/
public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
if ($input !== FALSE) {
if (!empty($input)) {
// If we're dealing with Mozilla or Opera, we're lucky. It will
// return a proper value, and we can get on with things.
return $element['#return_value'];
}
else {
// Unfortunately, in IE we never get back a proper value for THIS
// form element. Instead, we get back two split values: one for the
// X and one for the Y coordinates on which the user clicked the
// button. We'll find this element in the #post data, and search
// in the same spot for its name, with '_x'.
$input = $form_state->getUserInput();
foreach (explode('[', $element['#name']) as $element_name) {
// chop off the ] that may exist.
if (substr($element_name, -1) == ']') {
$element_name = substr($element_name, 0, -1);
}
if (!isset($input[$element_name])) {
if (isset($input[$element_name . '_x'])) {
return $element['#return_value'];
}
return NULL;
}
$input = $input[$element_name];
}
return $element['#return_value'];
}
}
}
/**
* {@inheritdoc}
*/
public static function preRenderButton($element) {
$element['#attributes']['type'] = 'image';
Element::setAttributes($element, array('id', 'name', 'value'));
$element['#attributes']['src'] = file_create_url($element['#src']);
if (!empty($element['#title'])) {
$element['#attributes']['alt'] = $element['#title'];
$element['#attributes']['title'] = $element['#title'];
}
$element['#attributes']['class'][] = 'image-button';
if (!empty($element['#button_type'])) {
$element['#attributes']['class'][] = 'image-button--' . $element['#button_type'];
}
$element['#attributes']['class'][] = 'js-form-submit';
$element['#attributes']['class'][] = 'form-submit';
if (!empty($element['#attributes']['disabled'])) {
$element['#attributes']['class'][] = 'is-disabled';
}
return $element;
}
}

View file

@ -0,0 +1,46 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\InlineTemplate.
*/
namespace Drupal\Core\Render\Element;
/**
* Provides a render element where the user supplies an in-line Twig template.
*
* @RenderElement("inline_template")
*/
class InlineTemplate extends RenderElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return array(
'#pre_render' => array(
array($class, 'preRenderInlineTemplate'),
),
'#template' => '',
'#context' => array(),
);
}
/**
* Renders a twig string directly.
*
* @param array $element
*
* @return array
*/
public static function preRenderInlineTemplate($element) {
/** @var \Drupal\Core\Template\TwigEnvironment $environment */
$environment = \Drupal::service('twig');
$markup = $environment->renderInline($element['#template'], $element['#context']);
$element['#markup'] = $markup;
return $element;
}
}

View file

@ -0,0 +1,37 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\Item.
*/
namespace Drupal\Core\Render\Element;
/**
* Provides a display-only form element with an optional title and description.
*
* Note: since this is a read-only field, setting the #required property will do
* nothing except theme the form element to look as if it were actually required
* (i.e. by placing a red star next to the #title).
*
* @FormElement("item")
*/
class Item extends FormElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
return array(
// Forms that show author fields to both anonymous and authenticated users
// need to dynamically switch between #type 'textfield' and #type 'item'
// to automatically take over the authenticated user's information.
// Therefore, we allow #type 'item' to receive input, which is internally
// assigned by Form API based on the #default_value or #value properties.
'#input' => TRUE,
'#markup' => '',
'#theme_wrappers' => array('form_element'),
);
}
}

View file

@ -0,0 +1,29 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\Label.
*/
namespace Drupal\Core\Render\Element;
/**
* Provides a render element for displaying the label for a form element.
*
* Labels are generated automatically from element properties during processing
* of most form elements.
*
* @RenderElement("label")
*/
class Label extends RenderElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
return array(
'#theme' => 'form_element_label',
);
}
}

View file

@ -0,0 +1,35 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\LanguageSelect.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Core\Language\LanguageInterface;
/**
* Provides a form element for selecting a language.
*
* This does not render an actual form element, but always returns the value of
* the default language. It is then extended by Language module via
* language_element_info_alter() to provide a proper language selector.
*
* @see language_element_info_alter()
*
* @FormElement("language_select")
*/
class LanguageSelect extends FormElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
return array(
'#input' => TRUE,
'#default_value' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
);
}
}

View file

@ -0,0 +1,92 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\Link.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\Html as HtmlUtility;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Url as CoreUrl;
/**
* 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().
* - #url: The URL info either pointing to a route or a non routed path.
* - #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 self::preRenderAjaxForm(), 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'] = HtmlUtility::getUniqueId('ajax-link');
}
$element = static::preRenderAjaxForm($element);
}
if (!empty($element['#url']) && $element['#url'] instanceof CoreUrl) {
$options = NestedArray::mergeDeep($element['#url']->getOptions(), $element['#options']);
/** @var \Drupal\Core\Utility\LinkGenerator $link_generator */
$link_generator = \Drupal::service('link_generator');
$generated_link = $link_generator->generate($element['#title'], $element['#url']->setOptions($options), TRUE);
$element['#markup'] = $generated_link->getGeneratedLink();
$generated_link->merge(CacheableMetadata::createFromRenderArray($element))
->applyTo($element);
}
return $element;
}
}

View file

@ -0,0 +1,218 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\MachineName.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
/**
* 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' => LanguageInterface::DIRECTION_LTR);
// The source element defaults to array('name'), but may have been overridden.
if (empty($element['#machine_name']['source'])) {
return $element;
}
// Retrieve the form element containing the human-readable name from the
// complete form in $form_state. By reference, because we may need to append
// a #field_suffix that will hold the live preview.
$key_exists = NULL;
$source = NestedArray::getValue($form_state->getCompleteForm(), $element['#machine_name']['source'], $key_exists);
if (!$key_exists) {
return $element;
}
$suffix_id = $source['#id'] . '-machine-name-suffix';
$element['#machine_name']['suffix'] = '#' . $suffix_id;
if ($element['#machine_name']['standalone']) {
$element['#suffix'] = $element['#suffix'] . ' <small id="' . $suffix_id . '">&nbsp;</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'] = $source['#field_suffix'] . ' <small id="' . $suffix_id . '">&nbsp;</small>';
$parents = array_merge($element['#machine_name']['source'], array('#field_suffix'));
NestedArray::setValue($form_state->getCompleteForm(), $parents, $source['#field_suffix']);
}
$element['#attached']['library'][] = 'core/drupal.machine-name';
$element['#attached']['drupalSettings']['machineName']['#' . $source['#id']] = $element['#machine_name'];
$element['#attached']['drupalSettings']['langcode'] = $language->getId();
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_state->setError($element, 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_state->setError($element, t('The machine-readable name must contain only lowercase letters, numbers, and hyphens.'));
}
// Otherwise, we assume the default (underscore).
else {
$form_state->setError($element, t('The machine-readable name must contain only lowercase letters, numbers, and underscores.'));
}
}
else {
$form_state->setError($element, $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_state->setError($element, t('The machine-readable name is already in use. It must be unique.'));
}
}
}
}

View file

@ -0,0 +1,32 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\MoreLink.
*/
namespace Drupal\Core\Render\Element;
/**
* Provides a link render element for a "more" link, like those used in blocks.
*
* @RenderElement("more_link")
*/
class MoreLink extends Link {
/**
* {@inheritdoc}
*/
public function getInfo() {
$info = parent::getInfo();
return array(
'#title' => $this->t('More'),
'#theme_wrappers' => array(
'container' => array(
'#attributes' => array('class' => array('more-link')),
),
),
) + $info;
}
}

View file

@ -0,0 +1,102 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\Number.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Component\Utility\Number as NumberUtility;
/**
* Provides a form element for numeric input, with special numeric validation.
*
* @FormElement("number")
*/
class Number extends FormElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return array(
'#input' => TRUE,
'#step' => 1,
'#process' => array(
array($class, 'processAjaxForm'),
),
'#element_validate' => array(
array($class, 'validateNumber'),
),
'#pre_render' => array(
array($class, 'preRenderNumber'),
),
'#theme' => 'input__number',
'#theme_wrappers' => array('form_element'),
);
}
/**
* Form element validation handler for #type 'number'.
*
* Note that #required is validated by _form_validate() already.
*/
public static function validateNumber(&$element, FormStateInterface $form_state, &$complete_form) {
$value = $element['#value'];
if ($value === '') {
return;
}
$name = empty($element['#title']) ? $element['#parents'][0] : $element['#title'];
// Ensure the input is numeric.
if (!is_numeric($value)) {
$form_state->setError($element, t('%name must be a number.', array('%name' => $name)));
return;
}
// Ensure that the input is greater than the #min property, if set.
if (isset($element['#min']) && $value < $element['#min']) {
$form_state->setError($element, t('%name must be higher than or equal to %min.', array('%name' => $name, '%min' => $element['#min'])));
}
// Ensure that the input is less than the #max property, if set.
if (isset($element['#max']) && $value > $element['#max']) {
$form_state->setError($element, t('%name must be lower than or equal to %max.', array('%name' => $name, '%max' => $element['#max'])));
}
if (isset($element['#step']) && strtolower($element['#step']) != 'any') {
// Check that the input is an allowed multiple of #step (offset by #min if
// #min is set).
$offset = isset($element['#min']) ? $element['#min'] : 0.0;
if (!NumberUtility::validStep($value, $element['#step'], $offset)) {
$form_state->setError($element, t('%name is not a valid number.', array('%name' => $name)));
}
}
}
/**
* Prepares a #type 'number' render element for input.html.twig.
*
* @param array $element
* An associative array containing the properties of the element.
* Properties used: #title, #value, #description, #min, #max, #placeholder,
* #required, #attributes, #step, #size.
*
* @return array
* The $element with prepared variables ready for input.html.twig.
*/
public static function preRenderNumber($element) {
$element['#attributes']['type'] = 'number';
Element::setAttributes($element, array('id', 'name', 'value', 'step', 'min', 'max', 'placeholder', 'size'));
static::setAttributes($element, array('form-number'));
return $element;
}
}

View file

@ -0,0 +1,30 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\Operations.
*/
namespace Drupal\Core\Render\Element;
/**
* Provides a render element for a set of operations links.
*
* This is a special case of \Drupal\Core\Render\Element\Dropbutton; the only
* difference is that it offers themes the possibility to render it differently
* through a theme suggestion.
*
* @RenderElement("operations")
*/
class Operations extends Dropbutton {
/**
* {@inheritdoc}
*/
public function getInfo() {
return array(
'#theme' => 'links__dropbutton__operations',
) + parent::getInfo();
}
}

View file

@ -0,0 +1,29 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\Page.
*/
namespace Drupal\Core\Render\Element;
/**
* Provides a render element for the content of an HTML page.
*
* This represents the "main part" of the HTML page's body; see html.html.twig.
*
* @RenderElement("page")
*/
class Page extends RenderElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
return array(
'#theme' => 'page',
'#title' => '',
);
}
}

View file

@ -0,0 +1,53 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\Pager.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Core\Render\Element;
/**
* Provides a render element for a pager.
*
* @RenderElement("pager")
*/
class Pager extends RenderElement{
/**
* {@inheritdoc}
*/
public function getInfo() {
return [
'#pre_render' => [
get_class($this) . '::preRenderPager',
],
'#theme' => 'pager',
// The pager ID, to distinguish between multiple pagers on the same page.
'#element' => 0,
// An associative array of query string parameters to append to the pager
// links.
'#parameters' => [],
// The number of pages in the list.
'#quantity' => 9,
// An array of labels for the controls in the pager.
'#tags' => [],
];
}
/**
* #pre_render callback to associate the appropriate cache context.
*
* @param array $pager
* A renderable array of #type => pager.
*
* @return array
*/
public static function preRenderPager(array $pager) {
$pager['#cache']['contexts'][] = 'url.query_args.pagers:' . $pager['#element'];
return $pager;
}
}

View file

@ -0,0 +1,59 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\Password.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Core\Render\Element;
/**
* Provides a form element for entering a password, with hidden text.
*
* @FormElement("password")
*/
class Password extends FormElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return array(
'#input' => TRUE,
'#size' => 60,
'#maxlength' => 128,
'#process' => array(
array($class, 'processAjaxForm'),
array($class, 'processPattern'),
),
'#pre_render' => array(
array($class, 'preRenderPassword'),
),
'#theme' => 'input__password',
'#theme_wrappers' => array('form_element'),
);
}
/**
* Prepares a #type 'password' render element for input.html.twig.
*
* @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 input.html.twig.
*/
public static function preRenderPassword($element) {
$element['#attributes']['type'] = 'password';
Element::setAttributes($element, array('id', 'name', 'size', 'maxlength', 'placeholder'));
static::setAttributes($element, array('form-text'));
return $element;
}
}

View file

@ -0,0 +1,101 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\PasswordConfirm.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides a form element for double-input of passwords.
*
* Formats as a pair of password fields, which do not validate unless the two
* entered passwords match.
*
* @FormElement("password_confirm")
*/
class PasswordConfirm extends FormElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return array(
'#input' => TRUE,
'#markup' => '',
'#process' => array(
array($class, 'processPasswordConfirm'),
),
'#theme_wrappers' => array('form_element'),
);
}
/**
* {@inheritdoc}
*/
public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
if ($input === FALSE) {
$element += array('#default_value' => array());
return $element['#default_value'] + array('pass1' => '', 'pass2' => '');
}
}
/**
* Expand a password_confirm field into two text boxes.
*/
public static function processPasswordConfirm(&$element, FormStateInterface $form_state, &$complete_form) {
$element['pass1'] = array(
'#type' => 'password',
'#title' => t('Password'),
'#value' => empty($element['#value']) ? NULL : $element['#value']['pass1'],
'#required' => $element['#required'],
'#attributes' => array('class' => array('password-field', 'js-password-field')),
'#error_no_message' => TRUE,
);
$element['pass2'] = array(
'#type' => 'password',
'#title' => t('Confirm password'),
'#value' => empty($element['#value']) ? NULL : $element['#value']['pass2'],
'#required' => $element['#required'],
'#attributes' => array('class' => array('password-confirm', 'js-password-confirm')),
'#error_no_message' => TRUE,
);
$element['#element_validate'] = array(array(get_called_class(), 'validatePasswordConfirm'));
$element['#tree'] = TRUE;
if (isset($element['#size'])) {
$element['pass1']['#size'] = $element['pass2']['#size'] = $element['#size'];
}
return $element;
}
/**
* Validates a password_confirm element.
*/
public static function validatePasswordConfirm(&$element, FormStateInterface $form_state, &$complete_form) {
$pass1 = trim($element['pass1']['#value']);
$pass2 = trim($element['pass2']['#value']);
if (!empty($pass1) || !empty($pass2)) {
if (strcmp($pass1, $pass2)) {
$form_state->setError($element, t('The specified passwords do not match.'));
}
}
elseif ($element['#required'] && $form_state->getUserInput()) {
$form_state->setError($element, t('Password field is required.'));
}
// Password field must be converted from a two-element array into a single
// string regardless of validation results.
$form_state->setValueForElement($element['pass1'], NULL);
$form_state->setValueForElement($element['pass2'], NULL);
$form_state->setValueForElement($element, $pass1);
return $element;
}
}

View file

@ -0,0 +1,98 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\PathElement.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides a matched path render element.
*
* Provides a form element to enter a path which can be optionally validated and
* stored as either a \Drupal\Core\Url value object or a array containing a
* route name and route parameters pair.
*
* @FormElement("path")
*/
class PathElement extends Textfield {
/**
* Do not convert the submitted value from the user-supplied path.
*/
const CONVERT_NONE = 0;
/**
* Convert the submitted value into a route name and parameter pair.
*/
const CONVERT_ROUTE = 1;
/**
* Convert the submitted value into a \Drupal\Core\Url value object.
*/
const CONVERT_URL = 2;
/**
* {@inheritdoc}
*/
public function getInfo() {
$info = parent::getInfo();
$class = get_class($this);
$info['#validate_path'] = TRUE;
$info['#convert_path'] = self::CONVERT_ROUTE;
$info['#element_validate'] = array(
array($class, 'validateMatchedPath'),
);
return $info;
}
/**
* {@inheritdoc}
*/
public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
return NULL;
}
/**
* Form element validation handler for matched_path elements.
*
* Note that #maxlength is validated by _form_validate() already.
*
* This checks that the submitted value matches an active route.
*/
public static function validateMatchedPath(&$element, FormStateInterface $form_state, &$complete_form) {
if (!empty($element['#value']) && ($element['#validate_path'] || $element['#convert_path'] != self::CONVERT_NONE)) {
/** @var \Drupal\Core\Url $url */
if ($url = \Drupal::service('path.validator')->getUrlIfValid($element['#value'])) {
if ($url->isExternal()) {
$form_state->setError($element, t('You cannot use an external URL, please enter a relative path.'));
return;
}
if ($element['#convert_path'] == self::CONVERT_NONE) {
// Url is valid, no conversion required.
return;
}
// We do the value conversion here whilst the Url object is in scope
// after validation has occurred.
if ($element['#convert_path'] == self::CONVERT_ROUTE) {
$form_state->setValueForElement($element, array(
'route_name' => $url->getRouteName(),
'route_parameters' => $url->getRouteParameters(),
));
return;
}
elseif ($element['#convert_path'] == self::CONVERT_URL) {
$form_state->setValueForElement($element, $url);
return;
}
}
$form_state->setError($element, t('This path does not exist or you do not have permission to link to %path.', array(
'%path' => $element['#value'],
)));
}
}
}

View file

@ -0,0 +1,72 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\Radio.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Core\Render\Element;
/**
* Provides a form element for a single radio button.
*
* This is an internal element that is primarily used to render the radios form
* element. Refer to \Drupal\Core\Render\Element\Radios for more documentation.
*
* @see \Drupal\Core\Render\Element\Radios
* @see \Drupal\Core\Render\Element\Checkbox
*
* @FormElement("radio")
*/
class Radio extends FormElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return array(
'#input' => TRUE,
'#default_value' => NULL,
'#process' => array(
array($class, 'processAjaxForm'),
),
'#pre_render' => array(
array($class, 'preRenderRadio'),
),
'#theme' => 'input__radio',
'#theme_wrappers' => array('form_element'),
'#title_display' => 'after',
);
}
/**
* Prepares a #type 'radio' render element for input.html.twig.
*
* @param array $element
* An associative array containing the properties of the element.
* Properties used: #required, #return_value, #value, #attributes, #title,
* #description.
*
* Note: The input "name" attribute needs to be sanitized before output, which
* is currently done by initializing Drupal\Core\Template\Attribute with
* all the attributes.
*
* @return array
* The $element with prepared variables ready for input.html.twig.
*/
public static function preRenderRadio($element) {
$element['#attributes']['type'] = 'radio';
Element::setAttributes($element, array('id', 'name', '#return_value' => 'value'));
if (isset($element['#return_value']) && $element['#value'] !== FALSE && $element['#value'] == $element['#return_value']) {
$element['#attributes']['checked'] = 'checked';
}
static::setAttributes($element, array('form-radio'));
return $element;
}
}

View file

@ -0,0 +1,124 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\Radios.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Component\Utility\Html as HtmlUtility;
/**
* Provides a form element for a set of radio buttons.
*
* Properties:
* - #options: An associative array, where the keys are the returned values for
* each radio button, and the values are the labels next to each radio button.
*
* Usage example:
* @code
* $form['settings']['active'] = array(
* '#type' => 'radios',
* '#title' => t('Poll status'),
* '#default_value' => 1
* '#options' => array(0 => t('Closed'), 1 => t('Active'),
* );
* @endcode
*
* @see \Drupal\Core\Render\Element\Checkboxes
* @see \Drupal\Core\Render\Element\Radio
* @see \Drupal\Core\Render\Element\Select
*
* @FormElement("radios")
*/
class Radios extends FormElement {
use CompositeFormElementTrait;
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return array(
'#input' => TRUE,
'#process' => array(
array($class, 'processRadios'),
),
'#theme_wrappers' => array('radios'),
'#pre_render' => array(
array($class, 'preRenderCompositeFormElement'),
),
);
}
/**
* Expands a radios element into individual radio elements.
*/
public static function processRadios(&$element, FormStateInterface $form_state, &$complete_form) {
if (count($element['#options']) > 0) {
$weight = 0;
foreach ($element['#options'] as $key => $choice) {
// Maintain order of options as defined in #options, in case the element
// defines custom option sub-elements, but does not define all option
// sub-elements.
$weight += 0.001;
$element += array($key => array());
// Generate the parents as the autogenerator does, so we will have a
// unique id for each radio button.
$parents_for_id = array_merge($element['#parents'], array($key));
$element[$key] += array(
'#type' => 'radio',
'#title' => $choice,
// The key is sanitized in Drupal\Core\Template\Attribute during output
// from the theme function.
'#return_value' => $key,
// Use default or FALSE. A value of FALSE means that the radio button is
// not 'checked'.
'#default_value' => isset($element['#default_value']) ? $element['#default_value'] : FALSE,
'#attributes' => $element['#attributes'],
'#parents' => $element['#parents'],
'#id' => HtmlUtility::getUniqueId('edit-' . implode('-', $parents_for_id)),
'#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL,
// Errors should only be shown on the parent radios element.
'#error_no_message' => TRUE,
'#weight' => $weight,
);
}
}
return $element;
}
/**
* {@inheritdoc}
*/
public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
if ($input !== FALSE) {
// When there's user input (including NULL), return it as the value.
// However, if NULL is submitted, FormBuilder::handleInputElement() will
// apply the default value, and we want that validated against #options
// unless it's empty. (An empty #default_value, such as NULL or FALSE, can
// be used to indicate that no radio button is selected by default.)
if (!isset($input) && !empty($element['#default_value'])) {
$element['#needs_validation'] = TRUE;
}
return $input;
}
else {
// For default value handling, simply return #default_value. Additionally,
// for a NULL default value, set #has_garbage_value to prevent
// FormBuilder::handleInputElement() converting the NULL to an empty
// string, so that code can distinguish between nothing selected and the
// selection of a radio button whose value is an empty string.
$value = isset($element['#default_value']) ? $element['#default_value'] : NULL;
if (!isset($value)) {
$element['#has_garbage_value'] = TRUE;
}
return $value;
}
}
}

View file

@ -0,0 +1,72 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\Range.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
/**
* Provides a form element for input of a number within a specific range.
*
* @FormElement("range")
*/
class Range extends Number {
/**
* {@inheritdoc}
*/
public function getInfo() {
$info = parent::getInfo();
$class = get_class($this);
return array(
'#min' => 0,
'#max' => 100,
'#pre_render' => array(
array($class, 'preRenderRange'),
),
'#theme' => 'input__range',
) + $info;
}
/**
* Prepares a #type 'range' render element for input.html.twig.
*
* @param array $element
* An associative array containing the properties of the element.
* Properties used: #title, #value, #description, #min, #max, #attributes,
* #step.
*
* @return array
* The $element with prepared variables ready for input.html.twig.
*/
public static function preRenderRange($element) {
$element['#attributes']['type'] = 'range';
Element::setAttributes($element, array('id', 'name', 'value', 'step', 'min', 'max'));
static::setAttributes($element, array('form-range'));
return $element;
}
/**
* {@inheritdoc}
*/
public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
if ($input === '') {
$offset = ($element['#max'] - $element['#min']) / 2;
// Round to the step.
if (strtolower($element['#step']) != 'any') {
$steps = round($offset / $element['#step']);
$offset = $element['#step'] * $steps;
}
return $element['#min'] + $offset;
}
}
}

View file

@ -0,0 +1,357 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\RenderElement.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\PluginBase;
use Drupal\Core\Render\Element;
use Drupal\Core\Url;
/**
* Provides a base class for render element 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 {
/**
* {@inheritdoc}
*/
public static function setAttributes(&$element, $class = array()) {
if (!empty($class)) {
if (!isset($element['#attributes']['class'])) {
$element['#attributes']['class'] = array();
}
$element['#attributes']['class'] = array_merge($element['#attributes']['class'], $class);
}
// This function is invoked from form element theme functions, but the
// rendered form element may not necessarily have been processed by
// \Drupal::formBuilder()->doBuildForm().
if (!empty($element['#required'])) {
$element['#attributes']['class'][] = 'required';
$element['#attributes']['required'] = 'required';
$element['#attributes']['aria-required'] = 'true';
}
if (isset($element['#parents']) && isset($element['#errors']) && !empty($element['#validated'])) {
$element['#attributes']['class'][] = 'error';
$element['#attributes']['aria-invalid'] = 'true';
}
}
/**
* Adds members of this group as actual elements for rendering.
*
* @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;
}
/**
* Form element processing handler for the #ajax form property.
*
* This method is useful for non-input elements that can be used in and
* outside the context of a form.
*
* @param array $element
* An associative array containing the properties of the element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param array $complete_form
* The complete form structure.
*
* @return array
* The processed element.
*
* @see self::preRenderAjaxForm()
*/
public static function processAjaxForm(&$element, FormStateInterface $form_state, &$complete_form) {
$element = static::preRenderAjaxForm($element);
// If the element was processed as an #ajax element, and a custom URL was
// provided, set the form to be cached.
if (!empty($element['#ajax_processed']) && !empty($element['#ajax']['url'])) {
$form_state->setCached();
}
return $element;
}
/**
* Adds Ajax information about an element to communicate with JavaScript.
*
* If #ajax is set on an element, this additional JavaScript is added to the
* page header to attach the Ajax behaviors. See ajax.js for more information.
*
* @param array $element
* An associative array containing the properties of the element.
* Properties used:
* - #ajax['event']
* - #ajax['prevent']
* - #ajax['url']
* - #ajax['callback']
* - #ajax['options']
* - #ajax['wrapper']
* - #ajax['parameters']
* - #ajax['effect']
* - #ajax['accepts']
*
* @return array
* The processed element with the necessary JavaScript attached to it.
*/
public static function preRenderAjaxForm($element) {
// Skip already processed elements.
if (isset($element['#ajax_processed'])) {
return $element;
}
// Initialize #ajax_processed, so we do not process this element again.
$element['#ajax_processed'] = FALSE;
// Nothing to do if there are no Ajax settings.
if (empty($element['#ajax'])) {
return $element;
}
// Add a reasonable default event handler if none was specified.
if (isset($element['#ajax']) && !isset($element['#ajax']['event'])) {
switch ($element['#type']) {
case 'submit':
case 'button':
case 'image_button':
// Pressing the ENTER key within a textfield triggers the click event of
// the form's first submit button. Triggering Ajax in this situation
// leads to problems, like breaking autocomplete textfields, so we bind
// to mousedown instead of click.
// @see https://www.drupal.org/node/216059
$element['#ajax']['event'] = 'mousedown';
// Retain keyboard accessibility by setting 'keypress'. This causes
// ajax.js to trigger 'event' when SPACE or ENTER are pressed while the
// button has focus.
$element['#ajax']['keypress'] = TRUE;
// Binding to mousedown rather than click means that it is possible to
// trigger a click by pressing the mouse, holding the mouse button down
// until the Ajax request is complete and the button is re-enabled, and
// then releasing the mouse button. Set 'prevent' so that ajax.js binds
// an additional handler to prevent such a click from triggering a
// non-Ajax form submission. This also prevents a textfield's ENTER
// press triggering this button's non-Ajax form submission behavior.
if (!isset($element['#ajax']['prevent'])) {
$element['#ajax']['prevent'] = 'click';
}
break;
case 'password':
case 'textfield':
case 'number':
case 'tel':
case 'textarea':
$element['#ajax']['event'] = 'blur';
break;
case 'radio':
case 'checkbox':
case 'select':
$element['#ajax']['event'] = 'change';
break;
case 'link':
$element['#ajax']['event'] = 'click';
break;
default:
return $element;
}
}
// Attach JavaScript settings to the element.
if (isset($element['#ajax']['event'])) {
$element['#attached']['library'][] = 'core/jquery.form';
$element['#attached']['library'][] = 'core/drupal.ajax';
$settings = $element['#ajax'];
// Assign default settings. When 'url' is set to NULL, ajax.js submits the
// Ajax request to the same URL as the form or link destination is for
// someone with JavaScript disabled. This is generally preferred as a way to
// ensure consistent server processing for js and no-js users, and Drupal's
// content negotiation takes care of formatting the response appropriately.
// However, 'url' and 'options' may be set when wanting server processing
// to be substantially different for a JavaScript triggered submission.
// One such substantial difference is form elements that use
// #ajax['callback'] for determining which part of the form needs
// re-rendering. For that, we have a special 'system.ajax' route which
// must be manually set.
$settings += [
'url' => NULL,
'options' => ['query' => []],
'dialogType' => 'ajax',
];
if (array_key_exists('callback', $settings) && !isset($settings['url'])) {
$settings['url'] = Url::fromRoute('<current>');
// Add all the current query parameters in order to ensure that we build
// the same form on the AJAX POST requests. For example,
// \Drupal\user\AccountForm takes query parameters into account in order
// to hide the password field dynamically.
$settings['options']['query'] += \Drupal::request()->query->all();
$settings['options']['query'][FormBuilderInterface::AJAX_FORM_REQUEST] = TRUE;
}
// @todo Legacy support. Remove in Drupal 8.
if (isset($settings['method']) && $settings['method'] == 'replace') {
$settings['method'] = 'replaceWith';
}
// Convert \Drupal\Core\Url object to string.
if (isset($settings['url']) && $settings['url'] instanceof Url) {
$settings['url'] = $settings['url']->setOptions($settings['options'])->toString();
}
else {
$settings['url'] = NULL;
}
unset($settings['options']);
// Add special data to $settings['submit'] so that when this element
// triggers an Ajax submission, Drupal's form processing can determine which
// element triggered it.
// @see _form_element_triggered_scripted_submission()
if (isset($settings['trigger_as'])) {
// An element can add a 'trigger_as' key within #ajax to make the element
// submit as though another one (for example, a non-button can use this
// to submit the form as though a button were clicked). When using this,
// the 'name' key is always required to identify the element to trigger
// as. The 'value' key is optional, and only needed when multiple elements
// share the same name, which is commonly the case for buttons.
$settings['submit']['_triggering_element_name'] = $settings['trigger_as']['name'];
if (isset($settings['trigger_as']['value'])) {
$settings['submit']['_triggering_element_value'] = $settings['trigger_as']['value'];
}
unset($settings['trigger_as']);
}
elseif (isset($element['#name'])) {
// Most of the time, elements can submit as themselves, in which case the
// 'trigger_as' key isn't needed, and the element's name is used.
$settings['submit']['_triggering_element_name'] = $element['#name'];
// If the element is a (non-image) button, its name may not identify it
// uniquely, in which case a match on value is also needed.
// @see _form_button_was_clicked()
if (!empty($element['#is_button']) && empty($element['#has_garbage_value'])) {
$settings['submit']['_triggering_element_value'] = $element['#value'];
}
}
// Convert a simple #ajax['progress'] string into an array.
if (isset($settings['progress']) && is_string($settings['progress'])) {
$settings['progress'] = array('type' => $settings['progress']);
}
// Change progress path to a full URL.
if (isset($settings['progress']['url']) && $settings['progress']['url'] instanceof Url) {
$settings['progress']['url'] = $settings['progress']['url']->toString();
}
$element['#attached']['drupalSettings']['ajax'][$element['#id']] = $settings;
// Indicate that Ajax processing was successful.
$element['#ajax_processed'] = TRUE;
}
return $element;
}
/**
* Arranges elements into groups.
*
* This method is useful for non-input elements that can be used in and
* outside the context of a form.
*
* @param array $element
* An associative array containing the properties and children of the
* element. Note that $element must be taken by reference here, so processed
* child elements are taken over into $form_state.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param array $complete_form
* The complete form structure.
*
* @return array
* The processed element.
*/
public static function processGroup(&$element, FormStateInterface $form_state, &$complete_form) {
$parents = implode('][', $element['#parents']);
// Each details element forms a new group. The #type 'vertical_tabs' basically
// only injects a new details element.
$groups = &$form_state->getGroups();
$groups[$parents]['#group_exists'] = TRUE;
$element['#groups'] = &$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'];
$groups[$group][] = &$element;
}
return $element;
}
}

View file

@ -0,0 +1,64 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\Search.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Core\Render\Element;
/**
* Provides a form input element for searching.
*
* This is commonly used to provide a filter or search box at the top of a
* long listing page, to allow users to find specific items in the list for
* faster input.
*
* @FormElement("search")
*/
class Search extends FormElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return array(
'#input' => TRUE,
'#size' => 60,
'#maxlength' => 128,
'#autocomplete_route_name' => FALSE,
'#process' => array(
array($class, 'processAutocomplete'),
array($class, 'processAjaxForm'),
),
'#pre_render' => array(
array($class, 'preRenderSearch'),
),
'#theme' => 'input__search',
'#theme_wrappers' => array('form_element'),
);
}
/**
* Prepares a #type 'search' render element for input.html.twig.
*
* @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 input.html.twig.
*/
public static function preRenderSearch($element) {
$element['#attributes']['type'] = 'search';
Element::setAttributes($element, array('id', 'name', 'value', 'size', 'maxlength', 'placeholder'));
static::setAttributes($element, array('form-search'));
return $element;
}
}

View file

@ -0,0 +1,163 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\Select.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
/**
* Provides a form element for a drop-down menu or scrolling selection box.
*
* Properties:
* - #options: An associative array, where the keys are the retured values for
* each option, and the values are the options to be shown in the drop-down
* list.
* - #empty_option: The label that will be displayed to denote no selection.
* - #empty_value: The value of the option that is used to denote no selection.
*
* @FormElement("select")
*/
class Select extends FormElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return array(
'#input' => TRUE,
'#multiple' => FALSE,
'#process' => array(
array($class, 'processSelect'),
array($class, 'processAjaxForm'),
),
'#pre_render' => array(
array($class, 'preRenderSelect'),
),
'#theme' => 'select',
'#theme_wrappers' => array('form_element'),
'#options' => array(),
);
}
/**
* Processes a select list form element.
*
* This process callback is mandatory for select fields, since all user agents
* automatically preselect the first available option of single (non-multiple)
* select lists.
*
* @param array $element
* The form element to process. Properties used:
* - #multiple: (optional) Indicates whether one or more options can be
* selected. Defaults to FALSE.
* - #default_value: Must be NULL or not set in case there is no value for the
* element yet, in which case a first default option is inserted by default.
* Whether this first option is a valid option depends on whether the field
* is #required or not.
* - #required: (optional) Whether the user needs to select an option (TRUE)
* or not (FALSE). Defaults to FALSE.
* - #empty_option: (optional) The label to show for the first default option.
* By default, the label is automatically set to "- Select -" for a required
* field and "- None -" for an optional field.
* - #empty_value: (optional) The value for the first default option, which is
* used to determine whether the user submitted a value or not.
* - If #required is TRUE, this defaults to '' (an empty string).
* - If #required is not TRUE and this value isn't set, then no extra option
* is added to the select control, leaving the control in a slightly
* illogical state, because there's no way for the user to select nothing,
* since all user agents automatically preselect the first available
* option. But people are used to this being the behavior of select
* controls.
* @todo Address the above issue in Drupal 8.
* - If #required is not TRUE and this value is set (most commonly to an
* empty string), then an extra option (see #empty_option above)
* representing a "non-selection" is added with this as its value.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param array $complete_form
* The complete form structure.
*
* @return array
* The processed element.
*
* @see _form_validate()
*/
public static function processSelect(&$element, FormStateInterface $form_state, &$complete_form) {
// #multiple select fields need a special #name.
if ($element['#multiple']) {
$element['#attributes']['multiple'] = 'multiple';
$element['#attributes']['name'] = $element['#name'] . '[]';
}
// A non-#multiple select needs special handling to prevent user agents from
// preselecting the first option without intention. #multiple select lists do
// not get an empty option, as it would not make sense, user interface-wise.
else {
// If the element is set to #required through #states, override the
// element's #required setting.
$required = isset($element['#states']['required']) ? TRUE : $element['#required'];
// If the element is required and there is no #default_value, then add an
// empty option that will fail validation, so that the user is required to
// make a choice. Also, if there's a value for #empty_value or
// #empty_option, then add an option that represents emptiness.
if (($required && !isset($element['#default_value'])) || isset($element['#empty_value']) || isset($element['#empty_option'])) {
$element += array(
'#empty_value' => '',
'#empty_option' => $required ? t('- Select -') : t('- None -'),
);
// The empty option is prepended to #options and purposively not merged
// to prevent another option in #options mistakenly using the same value
// as #empty_value.
$empty_option = array($element['#empty_value'] => $element['#empty_option']);
$element['#options'] = $empty_option + $element['#options'];
}
}
return $element;
}
/**
* {@inheritdoc}
*/
public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
if ($input !== FALSE) {
if (isset($element['#multiple']) && $element['#multiple']) {
// If an enabled multi-select submits NULL, it means all items are
// unselected. A disabled multi-select always submits NULL, and the
// default value should be used.
if (empty($element['#disabled'])) {
return (is_array($input)) ? array_combine($input, $input) : array();
}
else {
return (isset($element['#default_value']) && is_array($element['#default_value'])) ? $element['#default_value'] : array();
}
}
// Non-multiple select elements may have an empty option prepended to them
// (see \Drupal\Core\Render\Element\Select::processSelect()). When this
// occurs, usually #empty_value is an empty string, but some forms set
// #empty_value to integer 0 or some other non-string constant. PHP
// receives all submitted form input as strings, but if the empty option
// is selected, set the value to match the empty value exactly.
elseif (isset($element['#empty_value']) && $input === (string) $element['#empty_value']) {
return $element['#empty_value'];
}
else {
return $input;
}
}
}
/**
* Prepares a select render element.
*/
public static function preRenderSelect($element) {
Element::setAttributes($element, array('id', 'name', 'size'));
static::setAttributes($element, array('form-select'));
return $element;
}
}

View file

@ -0,0 +1,82 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\StatusMessages.
*/
namespace Drupal\Core\Render\Element;
/**
* Provides a messages element.
*
* @RenderElement("status_messages")
*/
class StatusMessages extends RenderElement {
/**
* {@inheritdoc}
*
* Generate the placeholder in a #pre_render callback, because the hash salt
* needs to be accessed, which may not yet be available when this is called.
*/
public function getInfo() {
return [
// May have a value of 'status' or 'error' when only displaying messages
// of that specific type.
'#display' => NULL,
'#pre_render' => [
get_class() . '::generatePlaceholder',
],
];
}
/**
* #pre_render callback to generate a placeholder.
*
* @param array $element
* A renderable array.
*
* @return array
* The updated renderable array containing the placeholder.
*/
public static function generatePlaceholder(array $element) {
$element['messages_placeholder'] = [
'#lazy_builder' => [get_class() . '::renderMessages', [$element['#display']]],
'#create_placeholder' => TRUE,
];
return $element;
}
/**
* #lazy_builder callback; replaces placeholder with messages.
*
* @param string|null $type
* Limit the messages returned by type. Defaults to NULL, meaning all types.
* Passed on to drupal_get_messages(). These values are supported:
* - NULL
* - 'status'
* - 'warning'
* - 'error'
*
* @return array
* A renderable array containing the messages.
*
* @see drupal_get_messages()
*/
public static function renderMessages($type) {
// Render the messages.
return [
'#theme' => 'status_messages',
// @todo Improve when https://www.drupal.org/node/2278383 lands.
'#message_list' => drupal_get_messages($type),
'#status_headings' => [
'status' => t('Status message'),
'error' => t('Error message'),
'warning' => t('Warning message'),
],
];
}
}

View file

@ -0,0 +1,29 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\Submit.
*/
namespace Drupal\Core\Render\Element;
/**
* Provides a form submit button.
*
* Submit buttons are processed the same as regular buttons, except they trigger
* the form's submit handler.
*
* @FormElement("submit")
*/
class Submit extends Button {
/**
* {@inheritdoc}
*/
public function getInfo() {
return array(
'#executes_submit_callback' => TRUE,
) + parent::getInfo();
}
}

View file

@ -0,0 +1,84 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\SystemCompactLink.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Core\Url as BaseUrl;
use Drupal\Component\Utility\NestedArray;
/**
* Provides a link render element to show or hide inline help descriptions.
*
* @RenderElement("system_compact_link")
*/
class SystemCompactLink extends Link {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return array(
'#pre_render' => array(
array($class, 'preRenderCompactLink'),
array($class, 'preRenderLink'),
),
'#theme_wrappers' => array(
'container' => array(
'#attributes' => array('class' => array('compact-link')),
),
),
);
}
/**
* 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 Drupal::l():
* - #title: The link text to pass as argument to Drupal::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 Drupal::l().
* - #options: (optional) An array of options to pass to Drupal::l() or the
* link generator.
*
* @return array
* The passed-in element containing the system compact link default values.
*/
public static function preRenderCompactLink($element) {
// By default, link options to pass to l() are normally set in #options.
$element += array('#options' => array());
if (system_admin_compact_mode()) {
$element['#title'] = t('Show descriptions');
$element['#url'] = BaseUrl::fromRoute('system.admin_compact_page', array('mode' => 'off'));
$element['#options'] = array(
'attributes' => array('title' => t('Expand layout to include descriptions.')),
'query' => \Drupal::destination()->getAsArray()
);
}
else {
$element['#title'] = t('Hide descriptions');
$element['#url'] = BaseUrl::fromRoute('system.admin_compact_page', array('mode' => 'on'));
$element['#options'] = array(
'attributes' => array('title' => t('Compress layout by hiding descriptions.')),
'query' => \Drupal::destination()->getAsArray(),
);
}
$options = NestedArray::mergeDeep($element['#url']->getOptions(), $element['#options']);
$element['#markup'] = \Drupal::l($element['#title'], $element['#url']->setOptions($options));
return $element;
}
}

View file

@ -0,0 +1,361 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\Table.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Component\Utility\Html as HtmlUtility;
/**
* Provides a render element for a table.
*
* Note: Although this extends FormElement, it can be used outside the
* context of a form.
*
* @see \Drupal\Core\Render\Element\Tableselect
*
* @FormElement("table")
*/
class Table extends FormElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return array(
'#header' => array(),
'#rows' => array(),
'#empty' => '',
// Properties for tableselect support.
'#input' => TRUE,
'#tree' => TRUE,
'#tableselect' => FALSE,
'#sticky' => FALSE,
'#responsive' => TRUE,
'#multiple' => TRUE,
'#js_select' => TRUE,
'#process' => array(
array($class, 'processTable'),
),
'#element_validate' => array(
array($class, 'validateTable'),
),
// Properties for tabledrag support.
// The value is a list of arrays that are passed to
// drupal_attach_tabledrag(). Table::preRenderTable() prepends the HTML ID
// of the table to each set of options.
// @see drupal_attach_tabledrag()
'#tabledrag' => array(),
// Render properties.
'#pre_render' => array(
array($class, 'preRenderTable'),
),
'#theme' => 'table',
);
}
/**
* {@inheritdoc}
*/
public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
// If #multiple is FALSE, the regular default value of radio buttons is used.
if (!empty($element['#tableselect']) && !empty($element['#multiple'])) {
// Contrary to #type 'checkboxes', the default value of checkboxes in a
// table is built from the array keys (instead of array values) of the
// #default_value property.
// @todo D8: Remove this inconsistency.
if ($input === FALSE) {
$element += array('#default_value' => array());
$value = array_keys(array_filter($element['#default_value']));
return array_combine($value, $value);
}
else {
return is_array($input) ? array_combine($input, $input) : array();
}
}
}
/**
* #process callback for #type 'table' to add tableselect support.
*
* @param array $element
* An associative array containing the properties and children of the
* table element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param array $complete_form
* The complete form structure.
*
* @return array
* The processed element.
*/
public static function processTable(&$element, FormStateInterface $form_state, &$complete_form) {
if ($element['#tableselect']) {
if ($element['#multiple']) {
$value = is_array($element['#value']) ? $element['#value'] : array();
}
// Advanced selection behavior makes no sense for radios.
else {
$element['#js_select'] = FALSE;
}
// Add a "Select all" checkbox column to the header.
// @todo D8: Rename into #select_all?
if ($element['#js_select']) {
$element['#attached']['library'][] = 'core/drupal.tableselect';
array_unshift($element['#header'], array('class' => array('select-all')));
}
// Add an empty header column for radio buttons or when a "Select all"
// checkbox is not desired.
else {
array_unshift($element['#header'], '');
}
if (!isset($element['#default_value']) || $element['#default_value'] === 0) {
$element['#default_value'] = array();
}
// Create a checkbox or radio for each row in a way that the value of the
// tableselect element behaves as if it had been of #type checkboxes or
// radios.
foreach (Element::children($element) as $key) {
$row = &$element[$key];
// Prepare the element #parents for the tableselect form element.
// Their values have to be located in child keys (#tree is ignored),
// since Table::validateTable() has to be able to validate whether input
// (for the parent #type 'table' element) has been submitted.
$element_parents = array_merge($element['#parents'], array($key));
// Since the #parents of the tableselect form element will equal the
// #parents of the row element, prevent FormBuilder from auto-generating
// an #id for the row element, since
// \Drupal\Component\Utility\Html::getUniqueId() would automatically
// append a suffix to the tableselect form element's #id otherwise.
$row['#id'] = HtmlUtility::getUniqueId('edit-' . implode('-', $element_parents) . '-row');
// Do not overwrite manually created children.
if (!isset($row['select'])) {
// Determine option label; either an assumed 'title' column, or the
// first available column containing a #title or #markup.
// @todo Consider to add an optional $element[$key]['#title_key']
// defaulting to 'title'?
unset($label_element);
$title = NULL;
if (isset($row['title']['#type']) && $row['title']['#type'] == 'label') {
$label_element = &$row['title'];
}
else {
if (!empty($row['title']['#title'])) {
$title = $row['title']['#title'];
}
else {
foreach (Element::children($row) as $column) {
if (isset($row[$column]['#title'])) {
$title = $row[$column]['#title'];
break;
}
if (isset($row[$column]['#markup'])) {
$title = $row[$column]['#markup'];
break;
}
}
}
if (isset($title) && $title !== '') {
$title = t('Update !title', array('!title' => $title));
}
}
// Prepend the select column to existing columns.
$row = array('select' => array()) + $row;
$row['select'] += array(
'#type' => $element['#multiple'] ? 'checkbox' : 'radio',
'#id' => HtmlUtility::getUniqueId('edit-' . implode('-', $element_parents)),
// @todo If rows happen to use numeric indexes instead of string keys,
// this results in a first row with $key === 0, which is always FALSE.
'#return_value' => $key,
'#attributes' => $element['#attributes'],
'#wrapper_attributes' => array(
'class' => array('table-select'),
),
);
if ($element['#multiple']) {
$row['select']['#default_value'] = isset($value[$key]) ? $key : NULL;
$row['select']['#parents'] = $element_parents;
}
else {
$row['select']['#default_value'] = ($element['#default_value'] == $key ? $key : NULL);
$row['select']['#parents'] = $element['#parents'];
}
if (isset($label_element)) {
$label_element['#id'] = $row['select']['#id'] . '--label';
$label_element['#for'] = $row['select']['#id'];
$row['select']['#attributes']['aria-labelledby'] = $label_element['#id'];
$row['select']['#title_display'] = 'none';
}
else {
$row['select']['#title'] = $title;
$row['select']['#title_display'] = 'invisible';
}
}
}
}
return $element;
}
/**
* #element_validate callback for #type 'table'.
*
* @param array $element
* An associative array containing the properties and children of the
* table element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param array $complete_form
* The complete form structure.
*/
public static function validateTable(&$element, FormStateInterface $form_state, &$complete_form) {
// Skip this validation if the button to submit the form does not require
// selected table row data.
$triggering_element = $form_state->getTriggeringElement();
if (empty($triggering_element['#tableselect'])) {
return;
}
if ($element['#multiple']) {
if (!is_array($element['#value']) || !count(array_filter($element['#value']))) {
$form_state->setError($element, t('No items selected.'));
}
}
elseif (!isset($element['#value']) || $element['#value'] === '') {
$form_state->setError($element, t('No item selected.'));
}
}
/**
* #pre_render callback to transform children of an element of #type 'table'.
*
* This function converts sub-elements of an element of #type 'table' to be
* suitable for table.html.twig:
* - The first level of sub-elements are table rows. Only the #attributes
* property is taken into account.
* - The second level of sub-elements is converted into columns for the
* corresponding first-level table row.
*
* Simple example usage:
* @code
* $form['table'] = array(
* '#type' => 'table',
* '#header' => array(t('Title'), array('data' => t('Operations'), 'colspan' => '1')),
* // Optionally, to add tableDrag support:
* '#tabledrag' => array(
* array(
* 'action' => 'order',
* 'relationship' => 'sibling',
* 'group' => 'thing-weight',
* ),
* ),
* );
* foreach ($things as $row => $thing) {
* $form['table'][$row]['#weight'] = $thing['weight'];
*
* $form['table'][$row]['title'] = array(
* '#type' => 'textfield',
* '#default_value' => $thing['title'],
* );
*
* // Optionally, to add tableDrag support:
* $form['table'][$row]['#attributes']['class'][] = 'draggable';
* $form['table'][$row]['weight'] = array(
* '#type' => 'textfield',
* '#title' => t('Weight for @title', array('@title' => $thing['title'])),
* '#title_display' => 'invisible',
* '#size' => 4,
* '#default_value' => $thing['weight'],
* '#attributes' => array('class' => array('thing-weight')),
* );
*
* // The amount of link columns should be identical to the 'colspan'
* // attribute in #header above.
* $form['table'][$row]['edit'] = array(
* '#type' => 'link',
* '#title' => t('Edit'),
* '#url' => Url::fromRoute('entity.test_entity.edit_form', ['test_entity' => $row]),
* );
* }
* @endcode
*
* @param array $element
* A structured array containing two sub-levels of elements. Properties used:
* - #tabledrag: The value is a list of $options arrays that are passed to
* drupal_attach_tabledrag(). The HTML ID of the table is added to each
* $options array.
*
* @return array
*
* @see template_preprocess_table()
* @see drupal_process_attached()
* @see drupal_attach_tabledrag()
*/
public static function preRenderTable($element) {
foreach (Element::children($element) as $first) {
$row = array('data' => array());
// Apply attributes of first-level elements as table row attributes.
if (isset($element[$first]['#attributes'])) {
$row += $element[$first]['#attributes'];
}
// Turn second-level elements into table row columns.
// @todo Do not render a cell for children of #type 'value'.
// @see https://www.drupal.org/node/1248940
foreach (Element::children($element[$first]) as $second) {
// Assign the element by reference, so any potential changes to the
// original element are taken over.
$column = array('data' => &$element[$first][$second]);
// Apply wrapper attributes of second-level elements as table cell
// attributes.
if (isset($element[$first][$second]['#wrapper_attributes'])) {
$column += $element[$first][$second]['#wrapper_attributes'];
}
$row['data'][] = $column;
}
$element['#rows'][] = $row;
}
// Take over $element['#id'] as HTML ID attribute, if not already set.
Element::setAttributes($element, array('id'));
// Add sticky headers, if applicable.
if (count($element['#header']) && $element['#sticky']) {
$element['#attached']['library'][] = 'core/drupal.tableheader';
// Add 'sticky-enabled' class to the table to identify it for JS.
// This is needed to target tables constructed by this function.
$element['#attributes']['class'][] = 'sticky-enabled';
}
// If the table has headers and it should react responsively to columns hidden
// with the classes represented by the constants RESPONSIVE_PRIORITY_MEDIUM
// and RESPONSIVE_PRIORITY_LOW, add the tableresponsive behaviors.
if (count($element['#header']) && $element['#responsive']) {
$element['#attached']['library'][] = 'core/drupal.tableresponsive';
// Add 'responsive-enabled' class to the table to identify it for JS.
// This is needed to target tables constructed by this function.
$element['#attributes']['class'][] = 'responsive-enabled';
}
// If the custom #tabledrag is set and there is a HTML ID, add the table's
// HTML ID to the options and attach the behavior.
if (!empty($element['#tabledrag']) && isset($element['#attributes']['id'])) {
foreach ($element['#tabledrag'] as $options) {
$options['table_id'] = $element['#attributes']['id'];
drupal_attach_tabledrag($element, $options);
}
}
return $element;
}
}

View file

@ -0,0 +1,261 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\Tableselect.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Component\Utility\Html as HtmlUtility;
/**
* Provides a form element for a table with radios or checkboxes in left column.
*
* Properties:
* - #headers: Table headers used in the table.
* - #options: An associative array where each key is the value returned when
* a user selects the radio button or checkbox, and each value is the row of
* table data.
*
* Usage example:
* See https://www.drupal.org/node/945102 for an example and full explanation.
*
* @see \Drupal\Core\Render\Element\Table
*
* @FormElement("tableselect")
*/
class Tableselect extends Table {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return array(
'#input' => TRUE,
'#js_select' => TRUE,
'#multiple' => TRUE,
'#responsive' => TRUE,
'#sticky' => FALSE,
'#pre_render' => array(
array($class, 'preRenderTable'),
array($class, 'preRenderTableselect'),
),
'#process' => array(
array($class, 'processTableselect'),
),
'#options' => array(),
'#empty' => '',
'#theme' => 'table__tableselect',
);
}
/**
* {@inheritdoc}
*/
public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
// If $element['#multiple'] == FALSE, then radio buttons are displayed and
// the default value handling is used.
if (isset($element['#multiple']) && $element['#multiple']) {
// Checkboxes are being displayed with the default value coming from the
// keys of the #default_value property. This differs from the checkboxes
// element which uses the array values.
if ($input === FALSE) {
$value = array();
$element += array('#default_value' => array());
foreach ($element['#default_value'] as $key => $flag) {
if ($flag) {
$value[$key] = $key;
}
}
return $value;
}
else {
return is_array($input) ? array_combine($input, $input) : array();
}
}
}
/**
* Prepares a 'tableselect' #type element for rendering.
*
* Adds a column of radio buttons or checkboxes for each row of a table.
*
* @param array $element
* An associative array containing the properties and children of
* the tableselect element. Properties used: #header, #options, #empty,
* and #js_select. The #options property is an array of selection options;
* each array element of #options is an array of properties. These
* properties can include #attributes, which is added to the
* table row's HTML attributes; see table.html.twig. An example of per-row
* options:
* @code
* $options = array(
* array(
* 'title' => 'How to Learn Drupal',
* 'content_type' => 'Article',
* 'status' => 'published',
* '#attributes' => array('class' => array('article-row')),
* ),
* array(
* 'title' => 'Privacy Policy',
* 'content_type' => 'Page',
* 'status' => 'published',
* '#attributes' => array('class' => array('page-row')),
* ),
* );
* $header = array(
* 'title' => t('Title'),
* 'content_type' => t('Content type'),
* 'status' => t('Status'),
* );
* $form['table'] = array(
* '#type' => 'tableselect',
* '#header' => $header,
* '#options' => $options,
* '#empty' => t('No content available.'),
* );
* @endcode
*
* @return array
* The processed element.
*/
public static function preRenderTableselect($element) {
$rows = array();
$header = $element['#header'];
if (!empty($element['#options'])) {
// Generate a table row for each selectable item in #options.
foreach (Element::children($element) as $key) {
$row = array();
$row['data'] = array();
if (isset($element['#options'][$key]['#attributes'])) {
$row += $element['#options'][$key]['#attributes'];
}
// Render the checkbox / radio element.
$row['data'][] = drupal_render($element[$key]);
// As table.html.twig only maps header and row columns by order, create
// the correct order by iterating over the header fields.
foreach ($element['#header'] as $fieldname => $title) {
// A row cell can span over multiple headers, which means less row
// cells than headers could be present.
if (isset($element['#options'][$key][$fieldname])) {
// A header can span over multiple cells and in this case the cells
// are passed in an array. The order of this array determines the
// order in which they are added.
if (is_array($element['#options'][$key][$fieldname]) && !isset($element['#options'][$key][$fieldname]['data'])) {
foreach ($element['#options'][$key][$fieldname] as $cell) {
$row['data'][] = $cell;
}
}
else {
$row['data'][] = $element['#options'][$key][$fieldname];
}
}
}
$rows[] = $row;
}
// Add an empty header or a "Select all" checkbox to provide room for the
// checkboxes/radios in the first table column.
if ($element['#js_select']) {
// Add a "Select all" checkbox.
$element['#attached']['library'][] = 'core/drupal.tableselect';
array_unshift($header, array('class' => array('select-all')));
}
else {
// Add an empty header when radio buttons are displayed or a "Select all"
// checkbox is not desired.
array_unshift($header, '');
}
}
$element['#header'] = $header;
$element['#rows'] = $rows;
return $element;
}
/**
* Creates checkbox or radio elements to populate a tableselect table.
*
* @param array $element
* An associative array containing the properties and children of the
* tableselect element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param array $complete_form
* The complete form structure.
*
* @return array
* The processed element.
*/
public static function processTableselect(&$element, FormStateInterface $form_state, &$complete_form) {
if ($element['#multiple']) {
$value = is_array($element['#value']) ? $element['#value'] : array();
}
else {
// Advanced selection behavior makes no sense for radios.
$element['#js_select'] = FALSE;
}
$element['#tree'] = TRUE;
if (count($element['#options']) > 0) {
if (!isset($element['#default_value']) || $element['#default_value'] === 0) {
$element['#default_value'] = array();
}
// Create a checkbox or radio for each item in #options in such a way that
// the value of the tableselect element behaves as if it had been of type
// checkboxes or radios.
foreach ($element['#options'] as $key => $choice) {
// Do not overwrite manually created children.
if (!isset($element[$key])) {
if ($element['#multiple']) {
$title = '';
if (!empty($element['#options'][$key]['title']['data']['#title'])) {
$title = t('Update @title', array(
'@title' => $element['#options'][$key]['title']['data']['#title'],
));
}
$element[$key] = array(
'#type' => 'checkbox',
'#title' => $title,
'#title_display' => 'invisible',
'#return_value' => $key,
'#default_value' => isset($value[$key]) ? $key : NULL,
'#attributes' => $element['#attributes'],
);
}
else {
// Generate the parents as the autogenerator does, so we will have a
// unique id for each radio button.
$parents_for_id = array_merge($element['#parents'], array($key));
$element[$key] = array(
'#type' => 'radio',
'#title' => '',
'#return_value' => $key,
'#default_value' => ($element['#default_value'] == $key) ? $key : NULL,
'#attributes' => $element['#attributes'],
'#parents' => $element['#parents'],
'#id' => HtmlUtility::getUniqueId('edit-' . implode('-', $parents_for_id)),
'#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL,
);
}
if (isset($element['#options'][$key]['#weight'])) {
$element[$key]['#weight'] = $element['#options'][$key]['#weight'];
}
}
}
}
else {
$element['#value'] = array();
}
return $element;
}
}

View file

@ -0,0 +1,61 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\Tel.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Core\Render\Element;
/**
* Provides a form element for entering a telephone number.
*
* @FormElement("tel")
*/
class Tel extends FormElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return array(
'#input' => TRUE,
'#size' => 30,
'#maxlength' => 128,
'#autocomplete_route_name' => FALSE,
'#process' => array(
array($class, 'processAutocomplete'),
array($class, 'processAjaxForm'),
array($class, 'processPattern'),
),
'#pre_render' => array(
array($class, 'preRenderTel'),
),
'#theme' => 'input__tel',
'#theme_wrappers' => array('form_element'),
);
}
/**
* Prepares a #type 'tel' render element for input.html.twig.
*
* @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 input.html.twig.
*/
public static function preRenderTel($element) {
$element['#attributes']['type'] = 'tel';
Element::setAttributes($element, array('id', 'name', 'value', 'size', 'maxlength', 'placeholder'));
static::setAttributes($element, array('form-tel'));
return $element;
}
}

View file

@ -0,0 +1,41 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\Textarea.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Core\Render\Element;
/**
* Provides a form element for input of multiple-line text.
*
* @FormElement("textarea")
*/
class Textarea extends FormElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return array(
'#input' => TRUE,
'#cols' => 60,
'#rows' => 5,
'#resizable' => 'vertical',
'#process' => array(
array($class, 'processAjaxForm'),
array($class, 'processGroup'),
),
'#pre_render' => array(
array($class, 'preRenderGroup'),
),
'#theme' => 'textarea',
'#theme_wrappers' => array('form_element'),
);
}
}

View file

@ -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 input.html.twig.
*
* @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 input.html.twig.
*/
public static function preRenderTextfield($element) {
$element['#attributes']['type'] = 'text';
Element::setAttributes($element, array('id', 'name', 'value', 'size', 'maxlength', 'placeholder'));
static::setAttributes($element, array('form-text'));
return $element;
}
}

View file

@ -0,0 +1,47 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\Token.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Core\Form\FormStateInterface;
/**
* Stores token data in a hidden form field.
*
* This is generally used to protect against cross-site forgeries. A token
* element is automatically added to each Drupal form by an implementation of
* \Drupal\Core\Form\FormBuilderInterface::prepareForm() so you don't generally
* have to add one yourself.
*
* @FormElement("token")
*/
class Token extends Hidden {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return array(
'#input' => TRUE,
'#pre_render' => array(
array($class, 'preRenderHidden'),
),
'#theme' => 'input__hidden',
);
}
/**
* {@inheritdoc}
*/
public static function valueCallback(&$element, $input, FormStateInterface $form_state) {
if ($input !== FALSE) {
return (string) $input;
}
}
}

View file

@ -0,0 +1,80 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\Url.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
/**
* Provides a form element for input of a URL.
*
* @FormElement("url")
*/
class Url extends FormElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return array(
'#input' => TRUE,
'#size' => 60,
'#maxlength' => 255,
'#autocomplete_route_name' => FALSE,
'#process' => array(
array($class, 'processAutocomplete'),
array($class, 'processAjaxForm'),
array($class, 'processPattern'),
),
'#element_validate' => array(
array($class, 'validateUrl'),
),
'#pre_render' => array(
array($class, 'preRenderUrl'),
),
'#theme' => 'input__url',
'#theme_wrappers' => array('form_element'),
);
}
/**
* Form element validation handler for #type 'url'.
*
* Note that #maxlength and #required is validated by _form_validate() already.
*/
public static function validateUrl(&$element, FormStateInterface $form_state, &$complete_form) {
$value = trim($element['#value']);
$form_state->setValueForElement($element, $value);
if ($value !== '' && !UrlHelper::isValid($value, TRUE)) {
$form_state->setError($element, t('The URL %url is not valid.', array('%url' => $value)));
}
}
/**
* Prepares a #type 'url' render element for input.html.twig.
*
* @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 input.html.twig.
*/
public static function preRenderUrl($element) {
$element['#attributes']['type'] = 'url';
Element::setAttributes($element, array('id', 'name', 'value', 'size', 'maxlength', 'placeholder'));
static::setAttributes($element, array('form-url'));
return $element;
}
}

View file

@ -0,0 +1,30 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\Value.
*/
namespace Drupal\Core\Render\Element;
/**
* Provides a form element for storage of internal information.
*
* Unlike \Drupal\Core\Render\Element\Hidden, this information is not
* sent to the browser in a hidden form field, but is just stored in the form
* array for use in validation and submit processing.
*
* @FormElement("value")
*/
class Value extends FormElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
return array(
'#input' => TRUE,
);
}
}

View file

@ -0,0 +1,110 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\VerticalTabs.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
/**
* Provides a render element for vertical tabs in a form.
*
* Formats all child fieldsets and all non-child fieldsets whose #group is
* assigned this element's name as vertical tabs.
*
* @FormElement("vertical_tabs")
*/
class VerticalTabs extends RenderElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return array(
'#default_tab' => '',
'#process' => array(
array($class, 'processVerticalTabs'),
),
'#pre_render' => array(
array($class, 'preRenderVerticalTabs'),
),
'#theme_wrappers' => array('vertical_tabs', 'form_element'),
);
}
/**
* Prepares a vertical_tabs element for rendering.
*
* @param array $element
* An associative array containing the properties and children of the
* vertical tabs element.
*
* @return array
* The modified element.
*/
public static function preRenderVerticalTabs($element) {
// Do not render the vertical tabs element if it is empty.
$group = implode('][', $element['#parents']);
if (!Element::getVisibleChildren($element['group']['#groups'][$group])) {
$element['#printed'] = TRUE;
}
return $element;
}
/**
* Creates a group formatted as vertical tabs.
*
* @param array $element
* An associative array containing the properties and children of the
* details element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param array $complete_form
* The complete form structure.
*
* @return array
* The processed element.
*/
public static function processVerticalTabs(&$element, FormStateInterface $form_state, &$complete_form) {
// Inject a new details as child, so that form_process_details() processes
// this details element like any other details.
$element['group'] = array(
'#type' => 'details',
'#theme_wrappers' => array(),
'#parents' => $element['#parents'],
);
// Add an invisible label for accessibility.
if (!isset($element['#title'])) {
$element['#title'] = t('Vertical Tabs');
$element['#title_display'] = 'invisible';
}
$element['#attached']['library'][] = 'core/drupal.vertical-tabs';
// The JavaScript stores the currently selected tab in this hidden
// field so that the active tab can be restored the next time the
// form is rendered, e.g. on preview pages or when form validation
// fails.
$name = implode('__', $element['#parents']);
if ($form_state->hasValue($name . '__active_tab')){
$element['#default_tab'] = $form_state->getValue($name . '__active_tab');
}
$element[$name . '__active_tab'] = array(
'#type' => 'hidden',
'#default_value' => $element['#default_tab'],
'#attributes' => array('class' => array('vertical-tabs-active-tab')),
);
// Clean up the active tab value so it's not accidentally stored in
// settings forms.
$form_state->addCleanValueKey($name . '__active_tab');
return $element;
}
}

View file

@ -0,0 +1,81 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Element\Weight.
*/
namespace Drupal\Core\Render\Element;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides a form element for input of a weight.
*
* Weights are integers used to indicate ordering, with larger numbers later in
* the order.
*
* Properties:
* - #delta: The range of possible weight values used. A delta of 10 would
* indicate possible weight values between -10 and 10.
*
* Usage example:
* @code
* $form['weight'] = array(
* '#type' => 'weight',
* '#title' => t('Weight'),
* '#default_value' => $edit['weight'],
* '#delta' => 10,
* );
* @endcode
*
* @FormElement("weight")
*/
class Weight extends FormElement {
/**
* {@inheritdoc}
*/
public function getInfo() {
$class = get_class($this);
return array(
'#input' => TRUE,
'#delta' => 10,
'#default_value' => 0,
'#process' => array(
array($class, 'processWeight'),
array($class, 'processAjaxForm'),
),
);
}
/**
* Expands a weight element into a select element.
*/
public static function processWeight(&$element, FormStateInterface $form_state, &$complete_form) {
$element['#is_weight'] = TRUE;
$element_info_manager = \Drupal::service('element_info');
// If the number of options is small enough, use a select field.
$max_elements = \Drupal::config('system.site')->get('weight_select_max');
if ($element['#delta'] <= $max_elements) {
$element['#type'] = 'select';
$weights = array();
for ($n = (-1 * $element['#delta']); $n <= $element['#delta']; $n++) {
$weights[$n] = $n;
}
$element['#options'] = $weights;
$element += $element_info_manager->getInfo('select');
}
// Otherwise, use a text field.
else {
$element['#type'] = 'number';
// Use a field big enough to fit most weights.
$element['#size'] = 10;
$element += $element_info_manager->getInfo('number');
}
return $element;
}
}

View file

@ -0,0 +1,173 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\ElementInfoManager.
*/
namespace Drupal\Core\Render;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\Render\Element\FormElementInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
/**
* 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;
/**
* The theme manager.
*
* @var \Drupal\Core\Theme\ThemeManagerInterface
*/
protected $themeManager;
/**
* The cache tag invalidator.
*
* @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface
*/
protected $cacheTagInvalidator;
/**
* 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\Cache\CacheTagsInvalidatorInterface $cache_tag_invalidator
* The cache tag invalidator.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to invoke the alter hook with.
* @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
* The theme manager.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, CacheTagsInvalidatorInterface $cache_tag_invalidator, ModuleHandlerInterface $module_handler, ThemeManagerInterface $theme_manager) {
$this->setCacheBackend($cache_backend, 'element_info');
$this->themeManager = $theme_manager;
$this->cacheTagInvalidator = $cache_tag_invalidator;
parent::__construct('Element', $namespaces, $module_handler, 'Drupal\Core\Render\Element\ElementInterface', 'Drupal\Core\Render\Annotation\RenderElement');
}
/**
* {@inheritdoc}
*/
public function getInfo($type) {
$theme_name = $this->themeManager->getActiveTheme()->getName();
if (!isset($this->elementInfo[$theme_name])) {
$this->elementInfo[$theme_name] = $this->buildInfo($theme_name);
}
$info = isset($this->elementInfo[$theme_name][$type]) ? $this->elementInfo[$theme_name][$type] : array();
$info['#defaults_loaded'] = TRUE;
return $info;
}
/**
* {@inheritdoc}
*/
public function getInfoProperty($type, $property_name, $default = NULL) {
$info = $this->getInfo($type);
return isset($info[$property_name]) ? $info[$property_name] : $default;
}
/**
* Builds up all element information.
*
* @param string $theme_name
* The theme name.
*
* @return array
*/
protected function buildInfo($theme_name) {
// Get cached definitions.
$cid = $this->getCid($theme_name);
if ($cache = $this->cacheBackend->get($cid)) {
return $cache->data;
}
// Otherwise, rebuild and cache.
// @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);
$this->themeManager->alter('element_info', $info);
$this->cacheBackend->set($cid, $info, Cache::PERMANENT, ['element_info_build']);
return $info;
}
/**
* {@inheritdoc}
*
* @return \Drupal\Core\Render\Element\ElementInterface
*/
public function createInstance($plugin_id, array $configuration = array()) {
return parent::createInstance($plugin_id, $configuration);
}
/**
* {@inheritdoc}
*/
public function clearCachedDefinitions() {
$this->elementInfo = NULL;
$this->cacheTagInvalidator->invalidateTags(['element_info_build']);
parent::clearCachedDefinitions();
}
/**
* Returns the CID used to cache the element info.
*
* @param string $theme_name
* The theme name.
*
* @return string
*/
protected function getCid($theme_name) {
return 'element_info_build:' . $theme_name;
}
}

View file

@ -0,0 +1,71 @@
<?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
* https://www.drupal.org/node/169815.
*
* @param string $type
* 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);
/**
* Retrieves a single property for the defined element type.
*
* @param string $type
* An element type as defined by hook_element_info().
* @param string $property_name
* The property within the element type that should be returned.
* @param $default
* (Optional) The value to return if the element type does not specify a
* value for the property. Defaults to NULL.
*
* @return string
* The property value of the defined element type. Or the provided
* default value, which can be NULL.
*/
public function getInfoProperty($type, $property_name, $default = NULL);
}

View file

@ -0,0 +1,46 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\HtmlResponse.
*/
namespace Drupal\Core\Render;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Cache\CacheableResponseInterface;
use Drupal\Core\Cache\CacheableResponseTrait;
use Symfony\Component\HttpFoundation\Response;
/**
* A response that contains and can expose cacheability metadata and attachments.
*
* Supports Drupal's caching concepts: cache tags for invalidation and cache
* contexts for variations.
*
* Supports Drupal's idea of #attached metadata: libraries, settings, http_header and html_head.
*
* @see \Drupal\Core\Cache\CacheableResponse
* @see \Drupal\Core\Render\AttachmentsInterface
* @see \Drupal\Core\Render\AttachmentsTrait
*/
class HtmlResponse extends Response implements CacheableResponseInterface, AttachmentsInterface {
use CacheableResponseTrait;
use AttachmentsTrait;
/**
* {@inheritdoc}
*/
public function setContent($content) {
// A render array can automatically be converted to a string and set the
// necessary metadata.
if (is_array($content) && (isset($content['#markup']))) {
$this->addCacheableDependency(CacheableMetadata::createFromRenderArray($content));
$this->setAttachments($content['#attached']);
$content = $content['#markup'];
}
parent::setContent($content);
}
}

View file

@ -0,0 +1,218 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\HtmlResponseAttachmentsProcessor.
*/
namespace Drupal\Core\Render;
use Drupal\Core\Asset\AssetCollectionRendererInterface;
use Drupal\Core\Asset\AssetResolverInterface;
use Drupal\Core\Asset\AttachedAssets;
use Drupal\Core\Config\ConfigFactoryInterface;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Processes attachments of HTML responses.
*
* @see template_preprocess_html()
* @see \Drupal\Core\Render\AttachmentsResponseProcessorInterface
* @see \Drupal\Core\Render\BareHtmlPageRenderer
* @see \Drupal\Core\Render\HtmlResponse
* @see \Drupal\Core\Render\MainContent\HtmlRenderer
*/
class HtmlResponseAttachmentsProcessor implements AttachmentsResponseProcessorInterface {
/**
* The asset resolver service.
*
* @var \Drupal\Core\Asset\AssetResolverInterface
*/
protected $assetResolver;
/**
* A config object for the system performance configuration.
*
* @var \Drupal\Core\Config\Config
*/
protected $config;
/**
* The CSS asset collection renderer service.
*
* @var \Drupal\Core\Asset\AssetCollectionRendererInterface
*/
protected $cssCollectionRenderer;
/**
* The JS asset collection renderer service.
*
* @var \Drupal\Core\Asset\AssetCollectionRendererInterface
*/
protected $jsCollectionRenderer;
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* Constructs a HtmlResponseAttachmentsProcessor object.
*
* @param \Drupal\Core\Asset\AssetResolverInterface $asset_resolver
* An asset resolver.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* A config factory for retrieving required config objects.
* @param \Drupal\Core\Asset\AssetCollectionRendererInterface $css_collection_renderer
* The CSS asset collection renderer.
* @param \Drupal\Core\Asset\AssetCollectionRendererInterface $js_collection_renderer
* The JS asset collection renderer.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer.
*/
public function __construct(AssetResolverInterface $asset_resolver, ConfigFactoryInterface $config_factory, AssetCollectionRendererInterface $css_collection_renderer, AssetCollectionRendererInterface $js_collection_renderer, RequestStack $request_stack, RendererInterface $renderer) {
$this->assetResolver = $asset_resolver;
$this->config = $config_factory->get('system.performance');
$this->cssCollectionRenderer = $css_collection_renderer;
$this->jsCollectionRenderer = $js_collection_renderer;
$this->requestStack = $request_stack;
$this->renderer = $renderer;
}
/**
* {@inheritdoc}
*/
public function processAttachments(AttachmentsInterface $response) {
// @todo Convert to assertion once https://www.drupal.org/node/2408013 lands
if (!$response instanceof HtmlResponse) {
throw new \InvalidArgumentException('\Drupal\Core\Render\HtmlResponse instance expected.');
}
$attached = $response->getAttachments();
// Get the placeholders from attached and then remove them.
$placeholders = $attached['html_response_placeholders'];
unset($attached['html_response_placeholders']);
$variables = $this->processAssetLibraries($attached, $placeholders);
// Handle all non-asset attachments. This populates drupal_get_html_head()
// and drupal_get_http_header().
$all_attached = ['#attached' => $attached];
drupal_process_attached($all_attached);
// Get HTML head elements - if present.
if (isset($placeholders['head'])) {
$variables['head'] = drupal_get_html_head(FALSE);
}
// Now replace the placeholders in the response content with the real data.
$this->renderPlaceholders($response, $placeholders, $variables);
// Finally set the headers on the response.
$headers = drupal_get_http_header();
$this->setHeaders($response, $headers);
return $response;
}
/**
* Processes asset libraries into render arrays.
*
* @param array $attached
* The attachments to process.
* @param array $placeholders
* The placeholders that exist in the response.
*
* @return array
* An array keyed by asset type, with keys:
* - styles
* - scripts
* - scripts_bottom
*/
protected function processAssetLibraries(array $attached, array $placeholders) {
$all_attached = ['#attached' => $attached];
$assets = AttachedAssets::createFromRenderArray($all_attached);
// Take Ajax page state into account, to allow for something like Turbolinks
// to be implemented without altering core.
// @see https://github.com/rails/turbolinks/
// @todo https://www.drupal.org/node/2497115 - Below line is broken due to ->request.
$ajax_page_state = $this->requestStack->getCurrentRequest()->request->get('ajax_page_state');
$assets->setAlreadyLoadedLibraries(isset($ajax_page_state) ? explode(',', $ajax_page_state['libraries']) : []);
$variables = [];
// Print styles - if present.
if (isset($placeholders['styles'])) {
// Optimize CSS if necessary, but only during normal site operation.
$optimize_css = !defined('MAINTENANCE_MODE') && $this->config->get('css.preprocess');
$variables['styles'] = $this->cssCollectionRenderer->render($this->assetResolver->getCssAssets($assets, $optimize_css));
}
// Print scripts - if any are present.
if (isset($placeholders['scripts']) || isset($placeholders['scripts_bottom'])) {
// Optimize JS if necessary, but only during normal site operation.
$optimize_js = !defined('MAINTENANCE_MODE') && $this->config->get('js.preprocess');
list($js_assets_header, $js_assets_footer) = $this->assetResolver->getJsAssets($assets, $optimize_js);
$variables['scripts'] = $this->jsCollectionRenderer->render($js_assets_header);
$variables['scripts_bottom'] = $this->jsCollectionRenderer->render($js_assets_footer);
}
return $variables;
}
/**
* Renders variables into HTML markup and replaces placeholders in the
* response content.
*
* @param \Drupal\Core\Render\HtmlResponse $response
* The HTML response to update.
* @param array $placeholders
* An array of placeholders, keyed by type with the placeholders
* present in the content of the response as values.
* @param array $variables
* The variables to render and replace, keyed by type with renderable
* arrays as values.
*/
protected function renderPlaceholders(HtmlResponse $response, array $placeholders, array $variables) {
$content = $response->getContent();
foreach ($placeholders as $type => $placeholder) {
if (isset($variables[$type])) {
$content = str_replace($placeholder, $this->renderer->renderPlain($variables[$type]), $content);
}
}
$response->setContent($content);
}
/**
* Sets headers on a response object.
*
* @param \Drupal\Core\Render\HtmlResponse $response
* The HTML response to update.
* @param array $headers
* The headers to set.
*/
protected function setHeaders(HtmlResponse $response, array $headers) {
foreach ($headers as $name => $value) {
// Drupal treats the HTTP response status code like a header, even though
// it really is not.
if ($name === 'status') {
$response->setStatusCode($value);
}
$response->headers->set($name, $value, FALSE);
}
}
}

View file

@ -0,0 +1,91 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\MainContent\AjaxRenderer.
*/
namespace Drupal\Core\Render\MainContent;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\AlertCommand;
use Drupal\Core\Ajax\InsertCommand;
use Drupal\Core\Ajax\PrependCommand;
use Drupal\Core\Render\ElementInfoManagerInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Default main content renderer for Ajax requests.
*/
class AjaxRenderer implements MainContentRendererInterface {
/**
* The controller resolver.
*
* @var \Drupal\Core\Controller\ControllerResolverInterface
*/
protected $controllerResolver;
/**
* The element info manager.
*
* @var \Drupal\Core\Render\ElementInfoManagerInterface
*/
protected $elementInfoManager;
/**
* Constructs a new AjaxRenderer instance.
*
* @param \Drupal\Core\Render\ElementInfoManagerInterface $element_info_manager
* The element info manager.
*/
public function __construct(ElementInfoManagerInterface $element_info_manager) {
$this->elementInfoManager = $element_info_manager;
}
/**
* {@inheritdoc}
*/
public function renderResponse(array $main_content, Request $request, RouteMatchInterface $route_match) {
$response = new AjaxResponse();
if (isset($main_content['#type']) && ($main_content['#type'] == 'ajax')) {
// Complex Ajax callbacks can return a result that contains an error
// message or a specific set of commands to send to the browser.
$main_content += $this->elementInfoManager->getInfo('ajax');
$error = $main_content['#error'];
if (!empty($error)) {
// Fall back to some default message otherwise use the specific one.
if (!is_string($error)) {
$error = 'An error occurred while handling the request: The server received invalid input.';
}
$response->addCommand(new AlertCommand($error));
}
}
$html = $this->drupalRenderRoot($main_content);
$response->setAttachments($main_content['#attached']);
// The selector for the insert command is NULL as the new content will
// replace the element making the Ajax call. The default 'replaceWith'
// behavior can be changed with #ajax['method'].
$response->addCommand(new InsertCommand(NULL, $html));
$status_messages = array('#type' => 'status_messages');
$output = $this->drupalRenderRoot($status_messages);
if (!empty($output)) {
$response->addCommand(new PrependCommand(NULL, $output));
}
return $response;
}
/**
* Wraps drupal_render_root().
*
* @todo Remove as part of https://www.drupal.org/node/2182149.
*/
protected function drupalRenderRoot(&$elements) {
return drupal_render_root($elements);
}
}

View file

@ -0,0 +1,97 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\MainContent\DialogRenderer.
*/
namespace Drupal\Core\Render\MainContent;
use Drupal\Component\Utility\Html;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\OpenDialogCommand;
use Drupal\Core\Controller\TitleResolverInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Default main content renderer for dialog requests.
*/
class DialogRenderer implements MainContentRendererInterface {
/**
* The title resolver.
*
* @var \Drupal\Core\Controller\TitleResolverInterface
*/
protected $titleResolver;
/**
* Constructs a new DialogRenderer.
*
* @param \Drupal\Core\Controller\TitleResolverInterface $title_resolver
* The title resolver.
*/
public function __construct(TitleResolverInterface $title_resolver) {
$this->titleResolver = $title_resolver;
}
/**
* {@inheritdoc}
*/
public function renderResponse(array $main_content, Request $request, RouteMatchInterface $route_match) {
$response = new AjaxResponse();
// First render the main content, because it might provide a title.
$content = drupal_render_root($main_content);
// Attach the library necessary for using the OpenDialogCommand and set the
// attachments for this Ajax response.
$main_content['#attached']['library'][] = 'core/drupal.dialog.ajax';
$response->setAttachments($main_content['#attached']);
// Determine the title: use the title provided by the main content if any,
// otherwise get it from the routing information.
$title = isset($main_content['#title']) ? $main_content['#title'] : $this->titleResolver->getTitle($request, $route_match->getRouteObject());
// Determine the dialog options and the target for the OpenDialogCommand.
$options = $request->request->get('dialogOptions', array());
$target = $this->determineTargetSelector($options, $route_match);
$response->addCommand(new OpenDialogCommand($target, $title, $content, $options));
return $response;
}
/**
* Determine the target selector for the OpenDialogCommand.
*
* @param array &$options
* The 'target' option, if set, is used, and then removed from $options.
* @param RouteMatchInterface $route_match
* When no 'target' option is set in $options, $route_match is used instead
* to determine the target.
*
* @return string
* The target selector.
*/
protected function determineTargetSelector(array &$options, RouteMatchInterface $route_match) {
// Generate the target wrapper for the dialog.
if (isset($options['target'])) {
// If the target was nominated in the incoming options, use that.
$target = $options['target'];
// Ensure the target includes the #.
if (substr($target, 0, 1) != '#') {
$target = '#' . $target;
}
// This shouldn't be passed on to jQuery.ui.dialog.
unset($options['target']);
}
else {
// Generate a target based on the route id.
$route_name = $route_match->getRouteName();
$target = '#' . Html::getUniqueId("drupal-dialog-$route_name");
}
return $target;
}
}

View file

@ -0,0 +1,297 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\MainContent\HtmlRenderer.
*/
namespace Drupal\Core\Render\MainContent;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Core\Controller\TitleResolverInterface;
use Drupal\Core\Display\PageVariantInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Render\HtmlResponse;
use Drupal\Core\Render\PageDisplayVariantSelectionEvent;
use Drupal\Core\Render\RenderCacheInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Render\RenderEvents;
use Drupal\Core\Routing\RouteMatchInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Default main content renderer for HTML requests.
*
* For attachment handling of HTML responses:
* @see template_preprocess_html()
* @see \Drupal\Core\Render\AttachmentsResponseProcessorInterface
* @see \Drupal\Core\Render\BareHtmlPageRenderer
* @see \Drupal\Core\Render\HtmlResponse
* @see \Drupal\Core\Render\HtmlResponseAttachmentsProcessor
*/
class HtmlRenderer implements MainContentRendererInterface {
/**
* The title resolver.
*
* @var \Drupal\Core\Controller\TitleResolverInterface
*/
protected $titleResolver;
/**
* The display variant manager.
*
* @var \Drupal\Component\Plugin\PluginManagerInterface
*/
protected $displayVariantManager;
/**
* The event dispatcher.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The renderer service.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* The render cache service.
*
* @var \Drupal\Core\Render\RenderCacheInterface
*/
protected $renderCache;
/**
* Constructs a new HtmlRenderer.
*
* @param \Drupal\Core\Controller\TitleResolverInterface $title_resolver
* The title resolver.
* @param \Drupal\Component\Plugin\PluginManagerInterface $display_variant_manager
* The display variant manager.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The event dispatcher.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer service.
* @param \Drupal\Core\Render\RenderCacheInterface $render_cache
* The render cache service.
*/
public function __construct(TitleResolverInterface $title_resolver, PluginManagerInterface $display_variant_manager, EventDispatcherInterface $event_dispatcher, ModuleHandlerInterface $module_handler, RendererInterface $renderer, RenderCacheInterface $render_cache) {
$this->titleResolver = $title_resolver;
$this->displayVariantManager = $display_variant_manager;
$this->eventDispatcher = $event_dispatcher;
$this->moduleHandler = $module_handler;
$this->renderer = $renderer;
$this->renderCache = $render_cache;
}
/**
* {@inheritdoc}
*
* The entire HTML: takes a #type 'page' and wraps it in a #type 'html'.
*/
public function renderResponse(array $main_content, Request $request, RouteMatchInterface $route_match) {
list($page, $title) = $this->prepare($main_content, $request, $route_match);
if (!isset($page['#type']) || $page['#type'] !== 'page') {
throw new \LogicException('Must be #type page');
}
$page['#title'] = $title;
// Now render the rendered page.html.twig template inside the html.html.twig
// template, and use the bubbled #attached metadata from $page to ensure we
// load all attached assets.
$html = [
'#type' => 'html',
'page' => $page,
];
// The special page regions will appear directly in html.html.twig, not in
// page.html.twig, hence add them here, just before rendering html.html.twig.
$this->buildPageTopAndBottom($html);
// @todo https://www.drupal.org/node/2495001 Make renderRoot return a
// cacheable render array directly.
$this->renderer->renderRoot($html);
$content = $this->renderCache->getCacheableRenderArray($html);
// Also associate the "rendered" cache tag. This allows us to invalidate the
// entire render cache, regardless of the cache bin.
$content['#cache']['tags'][] = 'rendered';
$response = new HtmlResponse($content, 200, [
'Content-Type' => 'text/html; charset=UTF-8',
]);
return $response;
}
/**
* Prepares the HTML body: wraps the main content in #type 'page'.
*
* @param array $main_content
* The render array representing the main content.
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object, for context.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match, for context.
*
* @return array
* An array with two values:
* 0. A #type 'page' render array.
* 1. The page title.
*
* @throws \LogicException
* If the selected display variant does not implement PageVariantInterface.
*/
protected function prepare(array $main_content, Request $request, RouteMatchInterface $route_match) {
// If the _controller result already is #type => page,
// we have no work to do: The "main content" already is an entire "page"
// (see html.html.twig).
if (isset($main_content['#type']) && $main_content['#type'] === 'page') {
$page = $main_content;
}
// Otherwise, render it as the main content of a #type => page, by selecting
// page display variant to do that and building that page display variant.
else {
// Select the page display variant to be used to render this main content,
// default to the built-in "simple page".
$event = new PageDisplayVariantSelectionEvent('simple_page', $route_match);
$this->eventDispatcher->dispatch(RenderEvents::SELECT_PAGE_DISPLAY_VARIANT, $event);
$variant_id = $event->getPluginId();
// We must render the main content now already, because it might provide a
// title. We set its $is_root_call parameter to FALSE, to ensure
// placeholders are not yet replaced. This is essentially "pre-rendering"
// the main content, the "full rendering" will happen in
// ::renderResponse().
// @todo Remove this once https://www.drupal.org/node/2359901 lands.
if (!empty($main_content)) {
$this->renderer->render($main_content, FALSE);
$main_content = $this->renderCache->getCacheableRenderArray($main_content) + [
'#title' => isset($main_content['#title']) ? $main_content['#title'] : NULL
];
}
// Instantiate the page display, and give it the main content.
$page_display = $this->displayVariantManager->createInstance($variant_id);
if (!$page_display instanceof PageVariantInterface) {
throw new \LogicException('Cannot render the main content for this page because the provided display variant does not implement PageVariantInterface.');
}
$page_display->setMainContent($main_content);
// Generate a #type => page render array using the page display variant,
// the page display will build the content for the various page regions.
$page = array(
'#type' => 'page',
);
$page += $page_display->build();
}
// $page is now fully built. Find all non-empty page regions, and add a
// theme wrapper function that allows them to be consistently themed.
$regions = \Drupal::theme()->getActiveTheme()->getRegions();
foreach ($regions as $region) {
if (!empty($page[$region])) {
$page[$region]['#theme_wrappers'][] = 'region';
$page[$region]['#region'] = $region;
}
}
// Allow hooks to add attachments to $page['#attached'].
$this->invokePageAttachmentHooks($page);
// Determine the title: use the title provided by the main content if any,
// otherwise get it from the routing information.
$title = isset($main_content['#title']) ? $main_content['#title'] : $this->titleResolver->getTitle($request, $route_match->getRouteObject());
return [$page, $title];
}
/**
* Invokes the page attachment hooks.
*
* @param array &$page
* A #type 'page' render array, for which the page attachment hooks will be
* invoked and to which the results will be added.
*
* @throws \LogicException
*
* @internal
*
* @see hook_page_attachments()
* @see hook_page_attachments_alter()
*/
public function invokePageAttachmentHooks(array &$page) {
// Modules can add attachments.
$attachments = [];
foreach ($this->moduleHandler->getImplementations('page_attachments') as $module) {
$function = $module . '_page_attachments';
$function($attachments);
}
if (array_diff(array_keys($attachments), ['#attached', '#cache']) !== []) {
throw new \LogicException('Only #attached and #cache may be set in hook_page_attachments().');
}
// Modules and themes can alter page attachments.
$this->moduleHandler->alter('page_attachments', $attachments);
\Drupal::theme()->alter('page_attachments', $attachments);
if (array_diff(array_keys($attachments), ['#attached', '#cache']) !== []) {
throw new \LogicException('Only #attached and #cache may be set in hook_page_attachments_alter().');
}
// Merge the attachments onto the $page render array.
$page = $this->renderer->mergeBubbleableMetadata($page, $attachments);
}
/**
* Invokes the page top and bottom hooks.
*
* @param array &$html
* A #type 'html' render array, for which the page top and bottom hooks will
* be invoked, and to which the 'page_top' and 'page_bottom' children (also
* render arrays) will be added (if non-empty).
*
* @throws \LogicException
*
* @internal
*
* @see hook_page_top()
* @see hook_page_bottom()
* @see html.html.twig
*/
public function buildPageTopAndBottom(array &$html) {
// Modules can add render arrays to the top and bottom of the page.
$page_top = [];
$page_bottom = [];
foreach ($this->moduleHandler->getImplementations('page_top') as $module) {
$function = $module . '_page_top';
$function($page_top);
}
foreach ($this->moduleHandler->getImplementations('page_bottom') as $module) {
$function = $module . '_page_bottom';
$function($page_bottom);
}
if (!empty($page_top)) {
$html['page_top'] = $page_top;
}
if (!empty($page_bottom)) {
$html['page_bottom'] = $page_bottom;
}
}
}

View file

@ -0,0 +1,38 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\MainContent\MainContentRendererInterface.
*/
namespace Drupal\Core\Render\MainContent;
use Drupal\Core\Routing\RouteMatchInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* The interface for "main content" (@code _controller @endcode) renderers.
*
* Classes implementing this interface are able to render the main content (as
* received from controllers) into a response of a certain format
* (HTML, JSON ) and/or in a certain decorated manner (e.g. in the case of the
* default HTML main content renderer: with a page display variant applied).
*/
interface MainContentRendererInterface {
/**
* Renders the main content render array into a response.
*
* @param array $main_content
* The render array representing the main content.
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object, for context.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match, for context.
*
* @return \Symfony\Component\HttpFoundation\Response
* The Response in the format that this implementation supports.
*/
public function renderResponse(array $main_content, Request $request, RouteMatchInterface $route_match);
}

View file

@ -0,0 +1,35 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\MainContent\MainContentRenderersPass.
*/
namespace Drupal\Core\Render\MainContent;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
/**
* Adds main_content_renderers parameter to the container.
*/
class MainContentRenderersPass implements CompilerPassInterface {
/**
* {@inheritdoc}
*
* Collects the available main content renderer service IDs into the
* main_content_renderers parameter, keyed by format.
*/
public function process(ContainerBuilder $container) {
$main_content_renderers = [];
foreach ($container->findTaggedServiceIds('render.main_content_renderer') as $id => $attributes_list) {
foreach ($attributes_list as $attributes) {
$format = $attributes['format'];
$main_content_renderers[$format] = $id;
}
}
$container->setParameter('main_content_renderers', $main_content_renderers);
}
}

View file

@ -0,0 +1,46 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\MainContent\ModalRenderer.
*/
namespace Drupal\Core\Render\MainContent;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\OpenModalDialogCommand;
use Drupal\Core\Render\MainContent\DialogRenderer;
use Drupal\Core\Routing\RouteMatchInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Default main content renderer for modal dialog requests.
*/
class ModalRenderer extends DialogRenderer {
/**
* {@inheritdoc}
*/
public function renderResponse(array $main_content, Request $request, RouteMatchInterface $route_match) {
$response = new AjaxResponse();
// First render the main content, because it might provide a title.
$content = drupal_render_root($main_content);
// Attach the library necessary for using the OpenModalDialogCommand and set
// the attachments for this Ajax response.
$main_content['#attached']['library'][] = 'core/drupal.dialog.ajax';
$response->setAttachments($main_content['#attached']);
// If the main content doesn't provide a title, use the title resolver.
$title = isset($main_content['#title']) ? $main_content['#title'] : $this->titleResolver->getTitle($request, $route_match->getRouteObject());
// Determine the title: use the title provided by the main content if any,
// otherwise get it from the routing information.
$options = $request->request->get('dialogOptions', array());
$response->addCommand(new OpenModalDialogCommand($title, $content, $options));
return $response;
}
}

View file

@ -0,0 +1,77 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\PageDisplayVariantSelectionEvent.
*/
namespace Drupal\Core\Render;
use Drupal\Core\Routing\RouteMatchInterface;
use Symfony\Component\EventDispatcher\Event;
/**
* Event fired when rendering main content, to select a page display variant.
*
* @see \Drupal\Core\Render\RenderEvents::SELECT_PAGE_DISPLAY_VARIANT
* @see \Drupal\Core\Render\MainContent\HtmlRenderer
*/
class PageDisplayVariantSelectionEvent extends Event {
/**
* The selected page display variant plugin ID.
*
* @var string
*/
protected $pluginId;
/**
* The current route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* Constructs the page display variant plugin selection event.
*
* @param string
* The ID of the page display variant plugin to use by default.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match, for context.
*/
public function __construct($plugin_id, RouteMatchInterface $route_match) {
$this->pluginId = $plugin_id;
$this->routeMatch = $route_match;
}
/**
* The selected page display variant plugin ID.
*
* @param string $plugin_id
* The ID of the page display variant plugin to use.
*/
public function setPluginId($plugin_id) {
$this->pluginId = $plugin_id;
}
/**
* The selected page display variant plugin ID.
*
* @return string;
*/
public function getPluginId() {
return $this->pluginId;
}
/**
* Gets the current route match.
*
* @return \Drupal\Core\Routing\RouteMatchInterface
* The current route match, for context.
*/
public function getRouteMatch() {
return $this->routeMatch;
}
}

View file

@ -0,0 +1,53 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Plugin\DisplayVariant\SimplePageVariant.
*/
namespace Drupal\Core\Render\Plugin\DisplayVariant;
use Drupal\Core\Display\PageVariantInterface;
use Drupal\Core\Display\VariantBase;
/**
* Provides a page display variant that simply renders the main content.
*
* @PageDisplayVariant(
* id = "simple_page",
* admin_label = @Translation("Simple page")
* )
*/
class SimplePageVariant extends VariantBase implements PageVariantInterface {
/**
* The render array representing the main content.
*
* @var array
*/
protected $mainContent;
/**
* {@inheritdoc}
*/
public function setMainContent(array $main_content) {
$this->mainContent = $main_content;
}
/**
* {@inheritdoc}
*/
public function build() {
$build = [
'content' => [
'main_content' => $this->mainContent,
'messages' => [
'#type' => 'status_messages',
'#weight' => -1000,
],
],
];
return $build;
}
}

View file

@ -0,0 +1,333 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\RenderCache.
*/
namespace Drupal\Core\Render;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\Context\CacheContextsManager;
use Drupal\Core\Cache\CacheFactoryInterface;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Wraps the caching logic for the render caching system.
*/
class RenderCache implements RenderCacheInterface {
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* The cache factory.
*
* @var \Drupal\Core\Cache\CacheFactoryInterface
*/
protected $cacheFactory;
/**
* The cache contexts manager.
*
* @var \Drupal\Core\Cache\Context\CacheContextsManager
*/
protected $cacheContextsManager;
/**
* Constructs a new RenderCache object.
*
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack.
* @param \Drupal\Core\Cache\CacheFactoryInterface $cache_factory
* The cache factory.
* @param \Drupal\Core\Cache\Context\CacheContextsManager $cache_contexts_manager
* The cache contexts manager.
*/
public function __construct(RequestStack $request_stack, CacheFactoryInterface $cache_factory, CacheContextsManager $cache_contexts_manager) {
$this->requestStack = $request_stack;
$this->cacheFactory = $cache_factory;
$this->cacheContextsManager = $cache_contexts_manager;
}
/**
* {@inheritdoc}
*/
public function get(array $elements) {
// Form submissions rely on the form being built during the POST request,
// and render caching of forms prevents this from happening.
// @todo remove the isMethodSafe() check when
// https://www.drupal.org/node/2367555 lands.
if (!$this->requestStack->getCurrentRequest()->isMethodSafe() || !$cid = $this->createCacheID($elements)) {
return FALSE;
}
$bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'render';
if (!empty($cid) && ($cache_bin = $this->cacheFactory->get($bin)) && $cache = $cache_bin->get($cid)) {
$cached_element = $cache->data;
// Two-tier caching: redirect to actual (post-bubbling) cache item.
// @see \Drupal\Core\Render\RendererInterface::render()
// @see ::set()
if (isset($cached_element['#cache_redirect'])) {
return $this->get($cached_element);
}
// Return the cached element.
return $cached_element;
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function set(array &$elements, array $pre_bubbling_elements) {
// Form submissions rely on the form being built during the POST request,
// and render caching of forms prevents this from happening.
// @todo remove the isMethodSafe() check when
// https://www.drupal.org/node/2367555 lands.
if (!$this->requestStack->getCurrentRequest()->isMethodSafe() || !$cid = $this->createCacheID($elements)) {
return FALSE;
}
$data = $this->getCacheableRenderArray($elements);
$bin = isset($elements['#cache']['bin']) ? $elements['#cache']['bin'] : 'render';
$expire = ($elements['#cache']['max-age'] === Cache::PERMANENT) ? Cache::PERMANENT : (int) $this->requestStack->getMasterRequest()->server->get('REQUEST_TIME') + $elements['#cache']['max-age'];
$cache = $this->cacheFactory->get($bin);
// Calculate the pre-bubbling CID.
$pre_bubbling_cid = $this->createCacheID($pre_bubbling_elements);
// Two-tier caching: detect different CID post-bubbling, create redirect,
// update redirect if different set of cache contexts.
// @see \Drupal\Core\Render\RendererInterface::render()
// @see ::get()
if ($pre_bubbling_cid && $pre_bubbling_cid !== $cid) {
// The cache redirection strategy we're implementing here is pretty
// simple in concept. Suppose we have the following render structure:
// - A (pre-bubbling, specifies #cache['keys'] = ['foo'])
// -- B (specifies #cache['contexts'] = ['b'])
//
// At the time that we're evaluating whether A's rendering can be
// retrieved from cache, we won't know the contexts required by its
// children (the children might not even be built yet), so cacheGet()
// will only be able to get what is cached for a $cid of 'foo'. But at
// the time we're writing to that cache, we do know all the contexts that
// were specified by all children, so what we need is a way to
// persist that information between the cache write and the next cache
// read. So, what we can do is store the following into 'foo':
// [
// '#cache_redirect' => TRUE,
// '#cache' => [
// ...
// 'contexts' => ['b'],
// ],
// ]
//
// This efficiently lets cacheGet() redirect to a $cid that includes all
// of the required contexts. The strategy is on-demand: in the case where
// there aren't any additional contexts required by children that aren't
// already included in the parent's pre-bubbled #cache information, no
// cache redirection is needed.
//
// When implementing this redirection strategy, special care is needed to
// resolve potential cache ping-pong problems. For example, consider the
// following render structure:
// - A (pre-bubbling, specifies #cache['keys'] = ['foo'])
// -- B (pre-bubbling, specifies #cache['contexts'] = ['b'])
// --- C (pre-bubbling, specifies #cache['contexts'] = ['c'])
// --- D (pre-bubbling, specifies #cache['contexts'] = ['d'])
//
// Additionally, suppose that:
// - C only exists for a 'b' context value of 'b1'
// - D only exists for a 'b' context value of 'b2'
// This is an acceptable variation, since B specifies that its contents
// vary on context 'b'.
//
// A naive implementation of cache redirection would result in the
// following:
// - When a request is processed where context 'b' = 'b1', what would be
// cached for a $pre_bubbling_cid of 'foo' is:
// [
// '#cache_redirect' => TRUE,
// '#cache' => [
// ...
// 'contexts' => ['b', 'c'],
// ],
// ]
// - When a request is processed where context 'b' = 'b2', we would
// retrieve the above from cache, but when following that redirection,
// get a cache miss, since we're processing a 'b' context value that
// has not yet been cached. Given the cache miss, we would continue
// with rendering the structure, perform the required context bubbling
// and then overwrite the above item with:
// [
// '#cache_redirect' => TRUE,
// '#cache' => [
// ...
// 'contexts' => ['b', 'd'],
// ],
// ]
// - Now, if a request comes in where context 'b' = 'b1' again, the above
// would redirect to a cache key that doesn't exist, since we have not
// yet cached an item that includes 'b'='b1' and something for 'd'. So
// we would process this request as a cache miss, at the end of which,
// we would overwrite the above item back to:
// [
// '#cache_redirect' => TRUE,
// '#cache' => [
// ...
// 'contexts' => ['b', 'c'],
// ],
// ]
// - The above would always result in accurate renderings, but would
// result in poor performance as we keep processing requests as cache
// misses even though the target of the redirection is cached, and
// it's only the redirection element itself that is creating the
// ping-pong problem.
//
// A way to resolve the ping-pong problem is to eventually reach a cache
// state where the redirection element includes all of the contexts used
// throughout all requests:
// [
// '#cache_redirect' => TRUE,
// '#cache' => [
// ...
// 'contexts' => ['b', 'c', 'd'],
// ],
// ]
//
// We can't reach that state right away, since we don't know what the
// result of future requests will be, but we can incrementally move
// towards that state by progressively merging the 'contexts' value
// across requests. That's the strategy employed below and tested in
// \Drupal\Tests\Core\Render\RendererBubblingTest::testConditionalCacheContextBubblingSelfHealing().
// The set of cache contexts for this element, including the bubbled ones,
// for which we are handling a cache miss.
$cache_contexts = $data['#cache']['contexts'];
// Get the contexts by which this element should be varied according to
// the current redirecting cache item, if any.
$stored_cache_contexts = [];
$stored_cache_tags = [];
if ($stored_cache_redirect = $cache->get($pre_bubbling_cid)) {
$stored_cache_contexts = $stored_cache_redirect->data['#cache']['contexts'];
$stored_cache_tags = $stored_cache_redirect->data['#cache']['tags'];
}
// Calculate the union of the cache contexts for this request and the
// stored cache contexts.
$merged_cache_contexts = Cache::mergeContexts($stored_cache_contexts, $cache_contexts);
// Stored cache contexts incomplete: this request causes cache contexts to
// be added to the redirecting cache item.
if (array_diff($merged_cache_contexts, $stored_cache_contexts)) {
$redirect_data = [
'#cache_redirect' => TRUE,
'#cache' => [
// The cache keys of the current element; this remains the same
// across requests.
'keys' => $elements['#cache']['keys'],
// The union of the current element's and stored cache contexts.
'contexts' => $merged_cache_contexts,
// The union of the current element's and stored cache tags.
'tags' => Cache::mergeTags($stored_cache_tags, $data['#cache']['tags']),
// The same cache bin as the one for the actual render cache items.
'bin' => $bin,
],
];
$cache->set($pre_bubbling_cid, $redirect_data, $expire, Cache::mergeTags($redirect_data['#cache']['tags'], ['rendered']));
}
// Current cache contexts incomplete: this request only uses a subset of
// the cache contexts stored in the redirecting cache item. Vary by these
// additional (conditional) cache contexts as well, otherwise the
// redirecting cache item would be pointing to a cache item that can never
// exist.
if (array_diff($merged_cache_contexts, $cache_contexts)) {
// Recalculate the cache ID.
$recalculated_cid_pseudo_element = [
'#cache' => [
'keys' => $elements['#cache']['keys'],
'contexts' => $merged_cache_contexts,
]
];
$cid = $this->createCacheID($recalculated_cid_pseudo_element);
// Ensure the about-to-be-cached data uses the merged cache contexts.
$data['#cache']['contexts'] = $merged_cache_contexts;
}
}
$cache->set($cid, $data, $expire, Cache::mergeTags($data['#cache']['tags'], ['rendered']));
}
/**
* Creates the cache ID for a renderable element.
*
* Creates the cache ID string based on #cache['keys'] + #cache['contexts'].
*
* @param array $elements
* A renderable array.
*
* @return string
* The cache ID string, or FALSE if the element may not be cached.
*/
protected function createCacheID(array $elements) {
// If the maximum age is zero, then caching is effectively prohibited.
if (isset($elements['#cache']['max-age']) && $elements['#cache']['max-age'] === 0) {
return FALSE;
}
if (isset($elements['#cache']['keys'])) {
$cid_parts = $elements['#cache']['keys'];
if (!empty($elements['#cache']['contexts'])) {
$contexts = $this->cacheContextsManager->convertTokensToKeys($elements['#cache']['contexts']);
$cid_parts = array_merge($cid_parts, $contexts);
}
return implode(':', $cid_parts);
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getCacheableRenderArray(array $elements) {
$data = [
'#markup' => $elements['#markup'],
'#attached' => $elements['#attached'],
'#cache' => [
'contexts' => $elements['#cache']['contexts'],
'tags' => $elements['#cache']['tags'],
'max-age' => $elements['#cache']['max-age'],
],
];
// Preserve cacheable items if specified. If we are preserving any cacheable
// children of the element, we assume we are only interested in their
// individual markup and not the parent's one, thus we empty it to minimize
// the cache entry size.
if (!empty($elements['#cache_properties']) && is_array($elements['#cache_properties'])) {
$data['#cache_properties'] = $elements['#cache_properties'];
// Extract all the cacheable items from the element using cache
// properties.
$cacheable_items = array_intersect_key($elements, array_flip($elements['#cache_properties']));
$cacheable_children = Element::children($cacheable_items);
if ($cacheable_children) {
$data['#markup'] = '';
// Cache only cacheable children's markup.
foreach ($cacheable_children as $key) {
$cacheable_items[$key] = ['#markup' => $cacheable_items[$key]['#markup']];
}
}
$data += $cacheable_items;
}
return $data;
}
}

View file

@ -0,0 +1,77 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\RenderCacheInterface.
*/
namespace Drupal\Core\Render;
/**
* Defines an interface for caching rendered render arrays.
*
* @see sec_caching
*
* @see \Drupal\Core\Render\RendererInterface
*/
interface RenderCacheInterface {
/**
* Gets a cacheable render array for a render array and its rendered output.
*
* Given a render array and its rendered output (HTML string), return an array
* data structure that allows the render array and its associated metadata to
* be cached reliably (and is serialization-safe).
*
* If Drupal needs additional rendering metadata to be cached at some point,
* consumers of this method will continue to work. Those who only cache
* certain parts of a render array will cease to work.
*
* @param array $elements
* A render array, on which \Drupal\Core\Render\RendererInterface::render()
* has already been invoked.
*
* @return array
* An array representing the cacheable data for this render array.
*/
public function getCacheableRenderArray(array $elements);
/**
* Gets the cached, pre-rendered element of a renderable element from cache.
*
* @param array $elements
* A renderable array.
*
* @return array|false
* A renderable array, with the original element and all its children pre-
* rendered, or FALSE if no cached copy of the element is available.
*
* @see \Drupal\Core\Render\RendererInterface::render()
* @see ::set()
*/
public function get(array $elements);
/**
* Caches the rendered output of a renderable array.
*
* May be called by an implementation of \Drupal\Core\Render\RendererInterface
* while rendering, if the #cache property is set.
*
* @param array $elements
* A renderable array.
* @param array $pre_bubbling_elements
* A renderable array corresponding to the state (in particular, the
* cacheability metadata) of $elements prior to the beginning of its
* rendering process, and therefore before any bubbling of child
* information has taken place. Only the #cache property is used by this
* function, so the caller may omit all other properties and children from
* this array.
*
* @return bool|null
* Returns FALSE if no cache item could be created, NULL otherwise.
*
* @see ::get()
*/
public function set(array &$elements, array $pre_bubbling_elements);
}

View file

@ -0,0 +1,30 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\RenderEvents.
*/
namespace Drupal\Core\Render;
/**
* Defines events for the render system.
*/
final class RenderEvents {
/**
* Name of the event when selecting a page display variant to use.
*
* This event allows you to select a different page display variant to use
* when rendering a page. The event listener method receives a
* \Drupal\Core\Render\PageDisplayVariantSelectionEvent instance.
*
* @Event
*
* @see \Drupal\Core\Render\PageDisplayVariantSelectionEvent
* @see \Drupal\Core\Render\MainContent\HtmlRenderer
* @see \Drupal\block\EventSubscriber\BlockPageDisplayVariantSubscriber
*/
const SELECT_PAGE_DISPLAY_VARIANT = 'render.page_display_variant.select';
}

View file

@ -0,0 +1,641 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\Renderer.
*/
namespace Drupal\Core\Render;
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Controller\ControllerResolverInterface;
use Drupal\Core\Template\Attribute;
use Drupal\Core\Theme\ThemeManagerInterface;
/**
* Turns a render array into a HTML string.
*/
class Renderer implements RendererInterface {
/**
* The theme manager.
*
* @var \Drupal\Core\Theme\ThemeManagerInterface
*/
protected $theme;
/**
* The controller resolver.
*
* @var \Drupal\Core\Controller\ControllerResolverInterface
*/
protected $controllerResolver;
/**
* The element info.
*
* @var \Drupal\Core\Render\ElementInfoManagerInterface
*/
protected $elementInfo;
/**
* The render cache service.
*
* @var \Drupal\Core\Render\RenderCacheInterface
*/
protected $renderCache;
/**
* The renderer configuration array.
*
* @var array
*/
protected $rendererConfig;
/**
* The stack containing bubbleable rendering metadata.
*
* @var \SplStack|null
*/
protected static $stack;
/**
* Constructs a new Renderer.
*
* @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
* The controller resolver.
* @param \Drupal\Core\Theme\ThemeManagerInterface $theme
* The theme manager.
* @param \Drupal\Core\Render\ElementInfoManagerInterface $element_info
* The element info.
* @param \Drupal\Core\Render\RenderCacheInterface $render_cache
* The render cache service.
* @param array $renderer_config
* The renderer configuration array.
*/
public function __construct(ControllerResolverInterface $controller_resolver, ThemeManagerInterface $theme, ElementInfoManagerInterface $element_info, RenderCacheInterface $render_cache, array $renderer_config) {
$this->controllerResolver = $controller_resolver;
$this->theme = $theme;
$this->elementInfo = $element_info;
$this->renderCache = $render_cache;
$this->rendererConfig = $renderer_config;
}
/**
* {@inheritdoc}
*/
public function renderRoot(&$elements) {
return $this->render($elements, TRUE);
}
/**
* {@inheritdoc}
*/
public function renderPlain(&$elements) {
$current_stack = static::$stack;
$this->resetStack();
$output = $this->renderRoot($elements);
static::$stack = $current_stack;
return $output;
}
/**
* Renders final HTML for a placeholder.
*
* Renders the placeholder in isolation.
*
* @param string $placeholder
* An attached placeholder to render. (This must be a key of one of the
* values of $elements['#attached']['placeholders'].)
* @param array $elements
* The structured array describing the data to be rendered.
*
* @return array
* The updated $elements.
*
* @see ::replacePlaceholders()
*
* @todo Make public as part of https://www.drupal.org/node/2469431
*/
protected function renderPlaceholder($placeholder, array $elements) {
// Get the render array for the given placeholder
$placeholder_elements = $elements['#attached']['placeholders'][$placeholder];
// Render the placeholder into markup.
$markup = $this->renderPlain($placeholder_elements);
// Replace the placeholder with its rendered markup, and merge its
// bubbleable metadata with the main elements'.
$elements['#markup'] = str_replace($placeholder, $markup, $elements['#markup']);
$elements = $this->mergeBubbleableMetadata($elements, $placeholder_elements);
// Remove the placeholder that we've just rendered.
unset($elements['#attached']['placeholders'][$placeholder]);
return $elements;
}
/**
* {@inheritdoc}
*/
public function render(&$elements, $is_root_call = FALSE) {
// Since #pre_render, #post_render, #lazy_builder callbacks and theme
// functions or templates may be used for generating a render array's
// content, and we might be rendering the main content for the page, it is
// possible that any of them throw an exception that will cause a different
// page to be rendered (e.g. throwing
// \Symfony\Component\HttpKernel\Exception\NotFoundHttpException will cause
// the 404 page to be rendered). That page might also use Renderer::render()
// but if exceptions aren't caught here, the stack will be left in an
// inconsistent state.
// Hence, catch all exceptions and reset the stack and re-throw them.
try {
return $this->doRender($elements, $is_root_call);
}
catch (\Exception $e) {
// Reset stack and re-throw exception.
$this->resetStack();
throw $e;
}
}
/**
* See the docs for ::render().
*/
protected function doRender(&$elements, $is_root_call = FALSE) {
if (!isset($elements['#access']) && isset($elements['#access_callback'])) {
if (is_string($elements['#access_callback']) && strpos($elements['#access_callback'], '::') === FALSE) {
$elements['#access_callback'] = $this->controllerResolver->getControllerFromDefinition($elements['#access_callback']);
}
$elements['#access'] = call_user_func($elements['#access_callback'], $elements);
}
// Early-return nothing if user does not have access.
if (empty($elements) || (isset($elements['#access']) && !$elements['#access'])) {
return '';
}
// Do not print elements twice.
if (!empty($elements['#printed'])) {
return '';
}
if (!isset(static::$stack)) {
static::$stack = new \SplStack();
}
static::$stack->push(new BubbleableMetadata());
// Set the bubbleable rendering metadata that has configurable defaults, if:
// - this is the root call, to ensure that the final render array definitely
// has these configurable defaults, even when no subtree is render cached.
// - this is a render cacheable subtree, to ensure that the cached data has
// the configurable defaults (which may affect the ID and invalidation).
if ($is_root_call || isset($elements['#cache']['keys'])) {
$required_cache_contexts = $this->rendererConfig['required_cache_contexts'];
if (isset($elements['#cache']['contexts'])) {
$elements['#cache']['contexts'] = Cache::mergeContexts($elements['#cache']['contexts'], $required_cache_contexts);
}
else {
$elements['#cache']['contexts'] = $required_cache_contexts;
}
}
// Try to fetch the prerendered element from cache, replace any placeholders
// and return the final markup.
if (isset($elements['#cache']['keys'])) {
$cached_element = $this->renderCache->get($elements);
if ($cached_element !== FALSE) {
$elements = $cached_element;
// Only when we're in a root (non-recursive) Renderer::render() call,
// placeholders must be processed, to prevent breaking the render cache
// in case of nested elements with #cache set.
if ($is_root_call) {
$this->replacePlaceholders($elements);
}
// Mark the element markup as safe. If we have cached children, we need
// to mark them as safe too. The parent markup contains the child
// markup, so if the parent markup is safe, then the markup of the
// individual children must be safe as well.
$elements['#markup'] = SafeMarkup::set($elements['#markup']);
if (!empty($elements['#cache_properties'])) {
foreach (Element::children($cached_element) as $key) {
SafeMarkup::set($cached_element[$key]['#markup']);
}
}
// The render cache item contains all the bubbleable rendering metadata
// for the subtree.
$this->updateStack($elements);
// Render cache hit, so rendering is finished, all necessary info
// collected!
$this->bubbleStack();
return $elements['#markup'];
}
}
// Two-tier caching: track pre-bubbling elements' #cache for later
// comparison.
// @see \Drupal\Core\Render\RenderCacheInterface::get()
// @see \Drupal\Core\Render\RenderCacheInterface::set()
$pre_bubbling_elements = [];
$pre_bubbling_elements['#cache'] = isset($elements['#cache']) ? $elements['#cache'] : [];
// If the default values for this element have not been loaded yet, populate
// them.
if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) {
$elements += $this->elementInfo->getInfo($elements['#type']);
}
// First validate the usage of #lazy_builder; both of the next if-statements
// use it if available.
if (isset($elements['#lazy_builder'])) {
// @todo Convert to assertions once https://www.drupal.org/node/2408013
// lands.
if (!is_array($elements['#lazy_builder'])) {
throw new \DomainException('The #lazy_builder property must have an array as a value.');
}
if (count($elements['#lazy_builder']) !== 2) {
throw new \DomainException('The #lazy_builder property must have an array as a value, containing two values: the callback, and the arguments for the callback.');
}
if (count($elements['#lazy_builder'][1]) !== count(array_filter($elements['#lazy_builder'][1], function($v) { return is_null($v) || is_scalar($v); }))) {
throw new \DomainException("A #lazy_builder callback's context may only contain scalar values or NULL.");
}
$children = Element::children($elements);
if ($children) {
throw new \DomainException(sprintf('When a #lazy_builder callback is specified, no children can exist; all children must be generated by the #lazy_builder callback. You specified the following children: %s.', implode(', ', $children)));
}
$supported_keys = [
'#lazy_builder',
'#cache',
'#create_placeholder',
// These keys are not actually supported, but they are added automatically
// by the Renderer, so we don't crash on them; them being missing when
// their #lazy_builder callback is invoked won't surprise the developer.
'#weight',
'#printed'
];
$unsupported_keys = array_diff(array_keys($elements), $supported_keys);
if (count($unsupported_keys)) {
throw new \DomainException(sprintf('When a #lazy_builder callback is specified, no properties can exist; all properties must be generated by the #lazy_builder callback. You specified the following properties: %s.', implode(', ', $unsupported_keys)));
}
}
// If instructed to create a placeholder, and a #lazy_builder callback is
// present (without such a callback, it would be impossible to replace the
// placeholder), replace the current element with a placeholder.
if (isset($elements['#create_placeholder']) && $elements['#create_placeholder'] === TRUE) {
if (!isset($elements['#lazy_builder'])) {
throw new \LogicException('When #create_placeholder is set, a #lazy_builder callback must be present as well.');
}
$elements = $this->createPlaceholder($elements);
}
// Build the element if it is still empty.
if (isset($elements['#lazy_builder'])) {
$callable = $elements['#lazy_builder'][0];
$args = $elements['#lazy_builder'][1];
if (is_string($callable) && strpos($callable, '::') === FALSE) {
$callable = $this->controllerResolver->getControllerFromDefinition($callable);
}
$new_elements = call_user_func_array($callable, $args);
// Retain the original cacheability metadata, plus cache keys.
CacheableMetadata::createFromRenderArray($elements)
->merge(CacheableMetadata::createFromRenderArray($new_elements))
->applyTo($new_elements);
if (isset($elements['#cache']['keys'])) {
$new_elements['#cache']['keys'] = $elements['#cache']['keys'];
}
$elements = $new_elements;
$elements['#lazy_builder_built'] = TRUE;
}
// Make any final changes to the element before it is rendered. This means
// that the $element or the children can be altered or corrected before the
// element is rendered into the final text.
if (isset($elements['#pre_render'])) {
foreach ($elements['#pre_render'] as $callable) {
if (is_string($callable) && strpos($callable, '::') === FALSE) {
$callable = $this->controllerResolver->getControllerFromDefinition($callable);
}
$elements = call_user_func($callable, $elements);
}
}
// Defaults for bubbleable rendering metadata.
$elements['#cache']['tags'] = isset($elements['#cache']['tags']) ? $elements['#cache']['tags'] : array();
$elements['#cache']['max-age'] = isset($elements['#cache']['max-age']) ? $elements['#cache']['max-age'] : Cache::PERMANENT;
$elements['#attached'] = isset($elements['#attached']) ? $elements['#attached'] : array();
// Allow #pre_render to abort rendering.
if (!empty($elements['#printed'])) {
// The #printed element contains all the bubbleable rendering metadata for
// the subtree.
$this->updateStack($elements);
// #printed, so rendering is finished, all necessary info collected!
$this->bubbleStack();
return '';
}
// Add any JavaScript state information associated with the element.
if (!empty($elements['#states'])) {
drupal_process_states($elements);
}
// Get the children of the element, sorted by weight.
$children = Element::children($elements, TRUE);
// Initialize this element's #children, unless a #pre_render callback
// already preset #children.
if (!isset($elements['#children'])) {
$elements['#children'] = '';
}
if (isset($elements['#markup'])) {
// @todo Decide how to support non-HTML in the render API in
// https://www.drupal.org/node/2501313.
$elements['#markup'] = SafeMarkup::checkAdminXss($elements['#markup']);
}
// Assume that if #theme is set it represents an implemented hook.
$theme_is_implemented = isset($elements['#theme']);
// Check the elements for insecure HTML and pass through sanitization.
if (isset($elements)) {
$markup_keys = array(
'#description',
'#field_prefix',
'#field_suffix',
);
foreach ($markup_keys as $key) {
if (!empty($elements[$key]) && is_scalar($elements[$key])) {
$elements[$key] = SafeMarkup::checkAdminXss($elements[$key]);
}
}
}
// Call the element's #theme function if it is set. Then any children of the
// element have to be rendered there. If the internal #render_children
// property is set, do not call the #theme function to prevent infinite
// recursion.
if ($theme_is_implemented && !isset($elements['#render_children'])) {
$elements['#children'] = $this->theme->render($elements['#theme'], $elements);
// If ThemeManagerInterface::render() returns FALSE this means that the
// hook in #theme was not found in the registry and so we need to update
// our flag accordingly. This is common for theme suggestions.
$theme_is_implemented = ($elements['#children'] !== FALSE);
}
// If #theme is not implemented or #render_children is set and the element
// has an empty #children attribute, render the children now. This is the
// same process as Renderer::render() but is inlined for speed.
if ((!$theme_is_implemented || isset($elements['#render_children'])) && empty($elements['#children'])) {
foreach ($children as $key) {
$elements['#children'] .= $this->doRender($elements[$key]);
}
$elements['#children'] = SafeMarkup::set($elements['#children']);
}
// If #theme is not implemented and the element has raw #markup as a
// fallback, prepend the content in #markup to #children. In this case
// #children will contain whatever is provided by #pre_render prepended to
// what is rendered recursively above. If #theme is implemented then it is
// the responsibility of that theme implementation to render #markup if
// required. Eventually #theme_wrappers will expect both #markup and
// #children to be a single string as #children.
if (!$theme_is_implemented && isset($elements['#markup'])) {
$elements['#children'] = SafeMarkup::set($elements['#markup'] . $elements['#children']);
}
// Let the theme functions in #theme_wrappers add markup around the rendered
// children.
// #states and #attached have to be processed before #theme_wrappers,
// because the #type 'page' render array from drupal_prepare_page() would
// render the $page and wrap it into the html.html.twig template without the
// attached assets otherwise.
// If the internal #render_children property is set, do not call the
// #theme_wrappers function(s) to prevent infinite recursion.
if (isset($elements['#theme_wrappers']) && !isset($elements['#render_children'])) {
foreach ($elements['#theme_wrappers'] as $key => $value) {
// If the value of a #theme_wrappers item is an array then the theme
// hook is found in the key of the item and the value contains attribute
// overrides. Attribute overrides replace key/value pairs in $elements
// for only this ThemeManagerInterface::render() call. This allows
// #theme hooks and #theme_wrappers hooks to share variable names
// without conflict or ambiguity.
$wrapper_elements = $elements;
if (is_string($key)) {
$wrapper_hook = $key;
foreach ($value as $attribute => $override) {
$wrapper_elements[$attribute] = $override;
}
}
else {
$wrapper_hook = $value;
}
$elements['#children'] = $this->theme->render($wrapper_hook, $wrapper_elements);
}
}
// Filter the outputted content and make any last changes before the content
// is sent to the browser. The changes are made on $content which allows the
// outputted text to be filtered.
if (isset($elements['#post_render'])) {
foreach ($elements['#post_render'] as $callable) {
if (is_string($callable) && strpos($callable, '::') === FALSE) {
$callable = $this->controllerResolver->getControllerFromDefinition($callable);
}
$elements['#children'] = call_user_func($callable, $elements['#children'], $elements);
}
}
// We store the resulting output in $elements['#markup'], to be consistent
// with how render cached output gets stored. This ensures that placeholder
// replacement logic gets the same data to work with, no matter if #cache is
// disabled, #cache is enabled, there is a cache hit or miss.
$prefix = isset($elements['#prefix']) ? SafeMarkup::checkAdminXss($elements['#prefix']) : '';
$suffix = isset($elements['#suffix']) ? SafeMarkup::checkAdminXss($elements['#suffix']) : '';
$elements['#markup'] = $prefix . $elements['#children'] . $suffix;
// We've rendered this element (and its subtree!), now update the stack.
$this->updateStack($elements);
// Cache the processed element if both $pre_bubbling_elements and $elements
// have the metadata necessary to generate a cache ID.
if (isset($pre_bubbling_elements['#cache']['keys']) && isset($elements['#cache']['keys'])) {
if ($pre_bubbling_elements['#cache']['keys'] !== $elements['#cache']['keys']) {
throw new \LogicException('Cache keys may not be changed after initial setup. Use the contexts property instead to bubble additional metadata.');
}
$this->renderCache->set($elements, $pre_bubbling_elements);
}
// Only when we're in a root (non-recursive) Renderer::render() call,
// placeholders must be processed, to prevent breaking the render cache in
// case of nested elements with #cache set.
//
// By running them here, we ensure that:
// - they run when #cache is disabled,
// - they run when #cache is enabled and there is a cache miss.
// Only the case of a cache hit when #cache is enabled, is not handled here,
// that is handled earlier in Renderer::render().
if ($is_root_call) {
$this->replacePlaceholders($elements);
if (static::$stack->count() !== 1) {
throw new \LogicException('A stray drupal_render() invocation with $is_root_call = TRUE is causing bubbling of attached assets to break.');
}
}
// Rendering is finished, all necessary info collected!
$this->bubbleStack();
$elements['#printed'] = TRUE;
$elements['#markup'] = SafeMarkup::set($elements['#markup']);
return $elements['#markup'];
}
/**
* Resets the renderer service's internal stack (used for bubbling metadata).
*
* Only necessary in very rare/advanced situations, such as when rendering an
* error page if an exception occurred *during* rendering.
*/
protected function resetStack() {
static::$stack = NULL;
}
/**
* Updates the stack.
*
* @param array &$element
* The element of the render array that has just been rendered. The stack
* frame for this element will be updated with the bubbleable rendering
* metadata of this element.
*/
protected function updateStack(&$element) {
// The latest frame represents the bubbleable metadata for the subtree.
$frame = static::$stack->pop();
// Update the frame, but also update the current element, to ensure it
// contains up-to-date information in case it gets render cached.
$updated_frame = BubbleableMetadata::createFromRenderArray($element)->merge($frame);
$updated_frame->applyTo($element);
static::$stack->push($updated_frame);
}
/**
* Bubbles the stack.
*
* Whenever another level in the render array has been rendered, the stack
* must be bubbled, to merge its rendering metadata with that of the parent
* element.
*/
protected function bubbleStack() {
// If there's only one frame on the stack, then this is the root call, and
// we can't bubble up further. Reset the stack for the next root call.
if (static::$stack->count() === 1) {
$this->resetStack();
return;
}
// Merge the current and the parent stack frame.
$current = static::$stack->pop();
$parent = static::$stack->pop();
static::$stack->push($current->merge($parent));
}
/**
* Replaces placeholders.
*
* Placeholders may have:
* - #lazy_builder callback, to build a render array to be rendered into
* markup that can replace the placeholder
* - #cache: to cache the result of the placeholder
*
* Also merges the bubbleable metadata resulting from the rendering of the
* contents of the placeholders. Hence $elements will be contain the entirety
* of bubbleable metadata.
*
* @param array &$elements
* The structured array describing the data being rendered. Including the
* bubbleable metadata associated with the markup that replaced the
* placeholders.
*
* @returns bool
* Whether placeholders were replaced.
*/
protected function replacePlaceholders(array &$elements) {
if (!isset($elements['#attached']['placeholders']) || empty($elements['#attached']['placeholders'])) {
return FALSE;
}
foreach (array_keys($elements['#attached']['placeholders']) as $placeholder) {
$elements = $this->renderPlaceholder($placeholder, $elements);
}
return TRUE;
}
/**
* Turns this element into a placeholder.
*
* Placeholdering allows us to avoid "poor cacheability contamination": this
* maps the current render array to one that only has #markup and #attached,
* and #attached contains a placeholder with this element's prior cacheability
* metadata. In other words: this placeholder is perfectly cacheable, the
* placeholder replacement logic effectively cordons off poor cacheability.
*
* @param array $element
* The render array to create a placeholder for.
*
* @return array
* Render array with placeholder markup and the attached placeholder
* replacement metadata.
*/
protected function createPlaceholder(array $element) {
$placeholder_render_array = array_intersect_key($element, [
// Placeholders are replaced with markup by executing the associated
// #lazy_builder callback, which generates a render array, and which the
// Renderer will render and replace the placeholder with.
'#lazy_builder' => TRUE,
// The cacheability metadata for the placeholder. The rendered result of
// the placeholder may itself be cached, if [#cache][keys] are specified.
'#cache' => TRUE,
]);
// Generate placeholder markup. Note that the only requirement is that this
// is unique markup that isn't easily guessable. The #lazy_builder callback
// and its arguments are put in the placeholder markup solely to simplify
// debugging.
$attributes = new Attribute();
$attributes['callback'] = $placeholder_render_array['#lazy_builder'][0];
$attributes['arguments'] = UrlHelper::buildQuery($placeholder_render_array['#lazy_builder'][1]);
$attributes['token'] = hash('sha1', serialize($placeholder_render_array));
$placeholder_markup = SafeMarkup::format('<drupal-render-placeholder@attributes></drupal-render-placeholder>', ['@attributes' => $attributes]);
// Build the placeholder element to return.
$placeholder_element = [];
$placeholder_element['#markup'] = $placeholder_markup;
$placeholder_element['#attached']['placeholders'][$placeholder_markup] = $placeholder_render_array;
return $placeholder_element;
}
/**
* {@inheritdoc}
*/
public function mergeBubbleableMetadata(array $a, array $b) {
$meta_a = BubbleableMetadata::createFromRenderArray($a);
$meta_b = BubbleableMetadata::createFromRenderArray($b);
$meta_a->merge($meta_b)->applyTo($a);
return $a;
}
/**
* {@inheritdoc}
*/
public function addCacheableDependency(array &$elements, $dependency) {
$meta_a = CacheableMetadata::createFromRenderArray($elements);
$meta_b = CacheableMetadata::createFromObject($dependency);
$meta_a->merge($meta_b)->applyTo($elements);
}
}

View file

@ -0,0 +1,352 @@
<?php
/**
* @file
* Contains \Drupal\Core\Render\RendererInterface.
*/
namespace Drupal\Core\Render;
/**
* Defines an interface for turning a render array into a string.
*/
interface RendererInterface {
/**
* Renders final HTML given a structured array tree.
*
* Calls ::render() in such a way that placeholders are replaced.
*
* Should therefore only be used in occasions where the final rendering is
* happening, just before sending a Response:
* - system internals that are responsible for rendering the final HTML
* - render arrays for non-HTML responses, such as feeds
*
* @param array $elements
* The structured array describing the data to be rendered.
*
* @return string
* The rendered HTML.
*
* @see ::render()
*/
public function renderRoot(&$elements);
/**
* Renders final HTML in situations where no assets are needed.
*
* Calls ::render() in such a way that placeholders are replaced.
*
* Useful for e.g. rendering the values of tokens or e-mails, which need a
* render array being turned into a string, but don't need any of the
* bubbleable metadata (the attached assets the cache tags).
*
* Some of these are a relatively common use case and happen *within* a
* ::renderRoot() call, but that is generally highly problematic (and hence an
* exception is thrown when a ::renderRoot() call happens within another
* ::renderRoot() call). However, in this case, we only care about the output,
* not about the bubbling. Hence this uses a separate render stack, to not
* affect the parent ::renderRoot() call.
*
* @param array $elements
* The structured array describing the data to be rendered.
*
* @return string
* The rendered HTML.
*
* @see ::renderRoot()
* @see ::render()
*/
public function renderPlain(&$elements);
/**
* Renders HTML given a structured array tree.
*
* Renderable arrays have two kinds of key/value pairs: properties and
* children. Properties have keys starting with '#' and their values influence
* how the array will be rendered. Children are all elements whose keys do not
* start with a '#'. Their values should be renderable arrays themselves,
* which will be rendered during the rendering of the parent array. The markup
* provided by the children is typically inserted into the markup generated by
* the parent array.
*
* An important aspect of rendering is caching the result, when appropriate.
* Because the HTML of a rendered item includes all of the HTML of the
* rendered children, caching it requires certain information to bubble up
* from child elements to their parents:
* - Cache contexts, so that the render cache is varied by every context that
* affects the rendered HTML. Because cache contexts affect the cache ID,
* and therefore must be resolved for cache hits as well as misses, it is
* up to the implementation of this interface to decide how to implement
* the caching of items whose children specify cache contexts not directly
* specified by the parent. \Drupal\Core\Render\Renderer implements this
* with a lazy two-tier caching strategy. Alternate strategies could be to
* not cache such parents at all or to cache them with the child elements
* replaced by placeholder tokens that are dynamically rendered after cache
* retrieval.
* - Cache tags, so that cached renderings are invalidated when site content
* or configuration that can affect that rendering changes.
* - Placeholders, with associated self-contained placeholder render arrays,
* for executing code to handle dynamic requirements that cannot be cached.
* A stack of \Drupal\Core\Render\BubbleableMetadata objects can be used to
* perform this bubbling.
*
* Additionally, whether retrieving from cache or not, it is important to
* know all of the assets (CSS and JavaScript) required by the rendered HTML,
* and this must also bubble from child to parent. Therefore,
* \Drupal\Core\Render\BubbleableMetadata includes that data as well.
*
* The process of rendering an element is recursive unless the element defines
* an implemented theme hook in #theme. During each call to
* Renderer::render(), the outermost renderable array (also known as an
* "element") is processed using the following steps:
* - If this element has already been printed (#printed = TRUE) or the user
* does not have access to it (#access = FALSE), then an empty string is
* returned.
* - If no stack data structure has been created yet, it is done now. Next,
* an empty \Drupal\Core\Render\BubbleableMetadata is pushed onto the
* stack.
* - If this element has #cache defined then the cached markup for this
* element will be returned if it exists in Renderer::render()'s cache. To
* use Renderer::render() caching, set the element's #cache property to an
* associative array with one or several of the following keys:
* - 'keys': An array of one or more keys that identify the element. If
* 'keys' is set, the cache ID is created automatically from these keys.
* - 'contexts': An array of one or more cache context IDs. These are
* converted to a final value depending on the request. (e.g. 'user' is
* mapped to the current user's ID.)
* - 'max-age': A time in seconds. Zero seconds means it is not cacheable.
* \Drupal\Core\Cache\Cache::PERMANENT means it is cacheable forever.
* - 'bin': Specify a cache bin to cache the element in. Default is
* 'default'.
* When there is a render cache hit, there is no rendering work left to be
* done, so the stack must be updated. The empty (and topmost) frame that
* was just pushed onto the stack is updated with all bubbleable rendering
* metadata from the element retrieved from render cache. Then, this stack
* frame is bubbled: the two topmost frames are popped from the stack,
* they are merged, and the result is pushed back onto the stack.
* However, also in case of a cache miss we have to do something. Note
* that a Renderer renders top-down, which means that we try to render a
* parent first, and we try to avoid the work of rendering the children by
* using the render cache. Though in this case, we are dealing with a
* cache miss. So a Renderer traverses down the tree, rendering all
* children. In doing so, the render stack is updated with the bubbleable
* metadata of the children. That means that once the children are
* rendered, we can render cache this element. But the cache ID may have
* *changed* at that point, because the children's cache contexts have
* been bubbled!
* It is for that case that we must store the current (pre-bubbling) cache
* ID, so that we can compare it with the new (post-bubbling) cache ID
* when writing to the cache. We store the current cache ID in
* $pre_bubbling_cid.
* - If this element has #type defined and the default attributes for this
* element have not already been merged in (#defaults_loaded = TRUE) then
* the defaults for this type of element, defined in hook_element_info(),
* are merged into the array. #defaults_loaded is set by functions that
* process render arrays and call the element info service before passing
* the array to Renderer::render(), such as form_builder() in the Form
* API.
* - If this element has #create_placeholder set to TRUE, and it has a
* #lazy_builder callback, then the element is replaced with another
* element that has only two properties: #markup and #attached. #markup
* will contain placeholder markup, and #attached contains the placeholder
* metadata, that will be used for replacing this placeholder. That
* metadata contains a very compact render array (containing only
* #lazy_builder and #cache) that will be rendered to replace the
* placeholder with its final markup. This means that when the
* #lazy_builder callback is called, it received a render array to add to
* that only contains #cache.
* - If this element has a #lazy_builder or an array of #pre_render
* functions defined, they are called sequentially to modify the element
* before rendering. #lazy_builder is preferred, since it allows for
* placeholdering (see previous step), but #pre_render is still supported.
* Both have their use case: #lazy_builder is for building a render array,
* #pre_render is for decorating an existing render array.
* After the #lazy_builder function is called, #lazy_builder is removed,
* and #built is set to TRUE.
* After the #lazy_builder and all #pre_render functions have been called,
* #printed is checked a second time in case a #lazy_builder or
* #pre_render function flags the element as printed. If #printed is set,
* we return early and hence no rendering work is left to be done,
* similarly to a render cache hit. Once again, the empty (and topmost)
* frame that was just pushed onto the stack is updated with all
* bubbleable rendering metadata from the element whose #printed = TRUE.
* Then, this stack frame is bubbled: the two topmost frames are popped
* from the stack, they are merged, and the result is pushed back onto the
* stack.
* - The child elements of this element are sorted by weight using uasort()
* in \Drupal\Core\Render\Element::children(). Since this is expensive,
* when passing already sorted elements to Renderer::render(), for example
* from a database query, set $elements['#sorted'] = TRUE to avoid sorting
* them a second time.
* - The main render phase to produce #children for this element takes
* place:
* - If this element has #theme defined and #theme is an implemented theme
* hook/suggestion then ThemeManagerInterface::render() is called and
* must render both the element and its children. If #render_children is
* set, ThemeManagerInterface::render() will not be called.
* #render_children is usually only set internally by
* ThemeManagerInterface::render() so that we can avoid the situation
* where Renderer::render() called from within a theme preprocess
* function creates an infinite loop.
* - If this element does not have a defined #theme, or the defined #theme
* hook is not implemented, or #render_children is set, then
* Renderer::render() is called recursively on each of the child
* elements of this element, and the result of each is concatenated onto
* #children. This is skipped if #children is not empty at this point.
* - Once #children has been rendered for this element, if #theme is not
* implemented and #markup is set for this element, #markup will be
* prepended to #children.
* - If this element has #states defined then JavaScript state information
* is added to this element's #attached attribute by
* drupal_process_states().
* - If this element has #attached defined then any required libraries,
* JavaScript, CSS, or other custom data are added to the current page by
* drupal_process_attached().
* - If this element has an array of #theme_wrappers defined and
* #render_children is not set, #children is then re-rendered by passing
* the element in its current state to ThemeManagerInterface::render()
* successively for each item in #theme_wrappers. Since #theme and
* #theme_wrappers hooks often define variables with the same names it is
* possible to explicitly override each attribute passed to each
* #theme_wrappers hook by setting the hook name as the key and an array
* of overrides as the value in #theme_wrappers array.
* For example, if we have a render element as follows:
* @code
* array(
* '#theme' => 'image',
* '#attributes' => array('class' => array('foo')),
* '#theme_wrappers' => array('container'),
* );
* @endcode
* and we need to pass the class 'bar' as an attribute for 'container', we
* can rewrite our element thus:
* @code
* array(
* '#theme' => 'image',
* '#attributes' => array('class' => array('foo')),
* '#theme_wrappers' => array(
* 'container' => array(
* '#attributes' => array('class' => array('bar')),
* ),
* ),
* );
* @endcode
* - If this element has an array of #post_render functions defined, they
* are called sequentially to modify the rendered #children. Unlike
* #pre_render functions, #post_render functions are passed both the
* rendered #children attribute as a string and the element itself.
* - If this element has #prefix and/or #suffix defined, they are
* concatenated to #children.
* - The rendering of this element is now complete. The next step will be
* render caching. So this is the perfect time to update the stack. At
* this point, children of this element (if any), have been rendered also,
* and if there were any, their bubbleable rendering metadata will have
* been bubbled up into the stack frame for the element that is currently
* being rendered. The render cache item for this element must contain the
* bubbleable rendering metadata for this element and all of its children.
* However, right now, the topmost stack frame (the one for this element)
* currently only contains the metadata for the children. Therefore, the
* topmost stack frame is updated with this element's metadata, and then
* the element's metadata is replaced with the metadata in the topmost
* stack frame. This element now contains all bubbleable rendering
* metadata for this element and all its children, so it's now ready for
* render caching.
* - If this element has #cache defined, the rendered output of this element
* is saved to Renderer::render()'s internal cache. This includes the
* changes made by #post_render.
* At the same time, if $pre_bubbling_cid is set, it is compared to the
* calculated cache ID. If they are different, then a redirecting cache
* item is created, containing the #cache metadata of the current element,
* and written to cache using the value of $pre_bubbling_cid as the cache
* ID. This ensures the pre-bubbling ("wrong") cache ID redirects to the
* post-bubbling ("right") cache ID.
* - If this element also has #cache_properties defined, all the array items
* matching the specified property names will be cached along with the
* element markup. If properties include children names, the system
* assumes only children's individual markup is relevant and ignores the
* parent markup. This approach is normally not needed and should be
* adopted only when dealing with very advanced use cases.
* - If this element has attached placeholders ([#attached][placeholders]),
* or any of its children has (which we would know thanks to the stack
* having been updated just before the render caching step), its
* placeholder element containing a #lazy_builder function is rendered in
* isolation. The resulting markup is used to replace the placeholder, and
* any bubbleable metadata is merged.
* Placeholders must be unique, to guarantee that e.g. samples of
* placeholders are not replaced as well.
* - Just before finishing the rendering of this element, this element's
* stack frame (the topmost one) is bubbled: the two topmost frames are
* popped from the stack, they are merged and the result is pushed back
* onto the stack.
* So if this element e.g. was a child element, then a new frame was
* pushed onto the stack element at the beginning of rendering this
* element, it was updated when the rendering was completed, and now we
* merge it with the frame for the parent, so that the parent now has the
* bubbleable rendering metadata for its child.
* - #printed is set to TRUE for this element to ensure that it is only
* rendered once.
* - The final value of #children for this element is returned as the
* rendered output.
*
* @param array $elements
* The structured array describing the data to be rendered.
* @param bool $is_root_call
* (Internal use only.) Whether this is a recursive call or not. See
* ::renderRoot().
*
* @return string
* The rendered HTML.
*
* @throws \LogicException
* If a root call to ::render() does not result in an empty stack, this
* indicates an erroneous ::render() root call (a root call within a
* root call, which makes no sense). Therefore, a logic exception is thrown.
* @throws \Exception
* If a #pre_render callback throws an exception, it is caught to reset the
* stack used for bubbling rendering metadata, and then the exception is re-
* thrown.
*
* @see \Drupal\Core\Render\ElementInfoManagerInterface::getInfo()
* @see \Drupal\Core\Theme\ThemeManagerInterface::render()
* @see drupal_process_states()
* @see drupal_process_attached()
* @see ::renderRoot()
*/
public function render(&$elements, $is_root_call = FALSE);
/**
* Merges the bubbleable rendering metadata o/t 2nd render array with the 1st.
*
* @param array $a
* A render array.
* @param array $b
* A render array.
*
* @return array
* The first render array, modified to also contain the bubbleable rendering
* metadata of the second render array.
*
* @see \Drupal\Core\Render\BubbleableMetadata
*/
public function mergeBubbleableMetadata(array $a, array $b);
/**
* Adds a dependency on an object: merges its cacheability metadata.
*
* E.g. when a render array depends on some configuration, an entity, or an
* access result, we must make sure their cacheability metadata is present on
* the render array. This method makes doing that simple.
*
* @param array &$elements
* The render array to update.
* @param \Drupal\Core\Cache\CacheableDependencyInterface|mixed $dependency
* The dependency. If the object implements CacheableDependencyInterface,
* then its cacheability metadata will be used. Otherwise, the passed in
* object must be assumed to be uncacheable, so max-age 0 is set.
*
* @see \Drupal\Core\Cache\CacheableMetadata::createFromObject()
*/
public function addCacheableDependency(array &$elements, $dependency);
}

File diff suppressed because it is too large Load diff