Drupal 8.0.0 beta 12. More info: https://www.drupal.org/node/2514176
This commit is contained in:
commit
9921556621
13277 changed files with 1459781 additions and 0 deletions
32
core/lib/Drupal/Core/Render/Annotation/FormElement.php
Normal file
32
core/lib/Drupal/Core/Render/Annotation/FormElement.php
Normal 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 {
|
||||
|
||||
}
|
34
core/lib/Drupal/Core/Render/Annotation/RenderElement.php
Normal file
34
core/lib/Drupal/Core/Render/Annotation/RenderElement.php
Normal 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 {
|
||||
|
||||
}
|
49
core/lib/Drupal/Core/Render/AttachmentsInterface.php
Normal file
49
core/lib/Drupal/Core/Render/AttachmentsInterface.php
Normal 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);
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
47
core/lib/Drupal/Core/Render/AttachmentsTrait.php
Normal file
47
core/lib/Drupal/Core/Render/AttachmentsTrait.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
81
core/lib/Drupal/Core/Render/BareHtmlPageRenderer.php
Normal file
81
core/lib/Drupal/Core/Render/BareHtmlPageRenderer.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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 = []);
|
||||
|
||||
}
|
148
core/lib/Drupal/Core/Render/BubbleableMetadata.php
Normal file
148
core/lib/Drupal/Core/Render/BubbleableMetadata.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
209
core/lib/Drupal/Core/Render/Element.php
Normal file
209
core/lib/Drupal/Core/Render/Element.php
Normal 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']);
|
||||
}
|
||||
|
||||
}
|
108
core/lib/Drupal/Core/Render/Element/Actions.php
Normal file
108
core/lib/Drupal/Core/Render/Element/Actions.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
36
core/lib/Drupal/Core/Render/Element/Ajax.php
Normal file
36
core/lib/Drupal/Core/Render/Element/Ajax.php
Normal 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,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
89
core/lib/Drupal/Core/Render/Element/Button.php
Normal file
89
core/lib/Drupal/Core/Render/Element/Button.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
139
core/lib/Drupal/Core/Render/Element/Checkbox.php
Normal file
139
core/lib/Drupal/Core/Render/Element/Checkbox.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
130
core/lib/Drupal/Core/Render/Element/Checkboxes.php
Normal file
130
core/lib/Drupal/Core/Render/Element/Checkboxes.php
Normal 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
82
core/lib/Drupal/Core/Render/Element/Color.php
Normal file
82
core/lib/Drupal/Core/Render/Element/Color.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
62
core/lib/Drupal/Core/Render/Element/Container.php
Normal file
62
core/lib/Drupal/Core/Render/Element/Container.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
68
core/lib/Drupal/Core/Render/Element/Date.php
Normal file
68
core/lib/Drupal/Core/Render/Element/Date.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
78
core/lib/Drupal/Core/Render/Element/Details.php
Normal file
78
core/lib/Drupal/Core/Render/Element/Details.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
52
core/lib/Drupal/Core/Render/Element/Dropbutton.php
Normal file
52
core/lib/Drupal/Core/Render/Element/Dropbutton.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
57
core/lib/Drupal/Core/Render/Element/ElementInterface.php
Normal file
57
core/lib/Drupal/Core/Render/Element/ElementInterface.php
Normal 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());
|
||||
|
||||
}
|
90
core/lib/Drupal/Core/Render/Element/Email.php
Normal file
90
core/lib/Drupal/Core/Render/Element/Email.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
29
core/lib/Drupal/Core/Render/Element/Fieldgroup.php
Normal file
29
core/lib/Drupal/Core/Render/Element/Fieldgroup.php
Normal 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();
|
||||
}
|
||||
|
||||
}
|
43
core/lib/Drupal/Core/Render/Element/Fieldset.php
Normal file
43
core/lib/Drupal/Core/Render/Element/Fieldset.php
Normal 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'),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
73
core/lib/Drupal/Core/Render/Element/File.php
Normal file
73
core/lib/Drupal/Core/Render/Element/File.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
29
core/lib/Drupal/Core/Render/Element/Form.php
Normal file
29
core/lib/Drupal/Core/Render/Element/Form.php
Normal 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'),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
131
core/lib/Drupal/Core/Render/Element/FormElement.php
Normal file
131
core/lib/Drupal/Core/Render/Element/FormElement.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
46
core/lib/Drupal/Core/Render/Element/FormElementInterface.php
Normal file
46
core/lib/Drupal/Core/Render/Element/FormElementInterface.php
Normal 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);
|
||||
|
||||
}
|
55
core/lib/Drupal/Core/Render/Element/Hidden.php
Normal file
55
core/lib/Drupal/Core/Render/Element/Hidden.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
30
core/lib/Drupal/Core/Render/Element/Html.php
Normal file
30
core/lib/Drupal/Core/Render/Element/Html.php
Normal 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'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
195
core/lib/Drupal/Core/Render/Element/HtmlTag.php
Normal file
195
core/lib/Drupal/Core/Render/Element/HtmlTag.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
98
core/lib/Drupal/Core/Render/Element/ImageButton.php
Normal file
98
core/lib/Drupal/Core/Render/Element/ImageButton.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
46
core/lib/Drupal/Core/Render/Element/InlineTemplate.php
Normal file
46
core/lib/Drupal/Core/Render/Element/InlineTemplate.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
37
core/lib/Drupal/Core/Render/Element/Item.php
Normal file
37
core/lib/Drupal/Core/Render/Element/Item.php
Normal 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'),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
29
core/lib/Drupal/Core/Render/Element/Label.php
Normal file
29
core/lib/Drupal/Core/Render/Element/Label.php
Normal 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',
|
||||
);
|
||||
}
|
||||
|
||||
}
|
35
core/lib/Drupal/Core/Render/Element/LanguageSelect.php
Normal file
35
core/lib/Drupal/Core/Render/Element/LanguageSelect.php
Normal 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,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
92
core/lib/Drupal/Core/Render/Element/Link.php
Normal file
92
core/lib/Drupal/Core/Render/Element/Link.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
218
core/lib/Drupal/Core/Render/Element/MachineName.php
Normal file
218
core/lib/Drupal/Core/Render/Element/MachineName.php
Normal 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 . '"> </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 . '"> </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.'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
32
core/lib/Drupal/Core/Render/Element/MoreLink.php
Normal file
32
core/lib/Drupal/Core/Render/Element/MoreLink.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
102
core/lib/Drupal/Core/Render/Element/Number.php
Normal file
102
core/lib/Drupal/Core/Render/Element/Number.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
30
core/lib/Drupal/Core/Render/Element/Operations.php
Normal file
30
core/lib/Drupal/Core/Render/Element/Operations.php
Normal 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();
|
||||
}
|
||||
|
||||
}
|
29
core/lib/Drupal/Core/Render/Element/Page.php
Normal file
29
core/lib/Drupal/Core/Render/Element/Page.php
Normal 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' => '',
|
||||
);
|
||||
}
|
||||
|
||||
}
|
53
core/lib/Drupal/Core/Render/Element/Pager.php
Normal file
53
core/lib/Drupal/Core/Render/Element/Pager.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
59
core/lib/Drupal/Core/Render/Element/Password.php
Normal file
59
core/lib/Drupal/Core/Render/Element/Password.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
101
core/lib/Drupal/Core/Render/Element/PasswordConfirm.php
Normal file
101
core/lib/Drupal/Core/Render/Element/PasswordConfirm.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
98
core/lib/Drupal/Core/Render/Element/PathElement.php
Normal file
98
core/lib/Drupal/Core/Render/Element/PathElement.php
Normal 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'],
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
72
core/lib/Drupal/Core/Render/Element/Radio.php
Normal file
72
core/lib/Drupal/Core/Render/Element/Radio.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
124
core/lib/Drupal/Core/Render/Element/Radios.php
Normal file
124
core/lib/Drupal/Core/Render/Element/Radios.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
72
core/lib/Drupal/Core/Render/Element/Range.php
Normal file
72
core/lib/Drupal/Core/Render/Element/Range.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
357
core/lib/Drupal/Core/Render/Element/RenderElement.php
Normal file
357
core/lib/Drupal/Core/Render/Element/RenderElement.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
64
core/lib/Drupal/Core/Render/Element/Search.php
Normal file
64
core/lib/Drupal/Core/Render/Element/Search.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
163
core/lib/Drupal/Core/Render/Element/Select.php
Normal file
163
core/lib/Drupal/Core/Render/Element/Select.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
82
core/lib/Drupal/Core/Render/Element/StatusMessages.php
Normal file
82
core/lib/Drupal/Core/Render/Element/StatusMessages.php
Normal 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'),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
29
core/lib/Drupal/Core/Render/Element/Submit.php
Normal file
29
core/lib/Drupal/Core/Render/Element/Submit.php
Normal 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();
|
||||
}
|
||||
|
||||
}
|
84
core/lib/Drupal/Core/Render/Element/SystemCompactLink.php
Normal file
84
core/lib/Drupal/Core/Render/Element/SystemCompactLink.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
361
core/lib/Drupal/Core/Render/Element/Table.php
Normal file
361
core/lib/Drupal/Core/Render/Element/Table.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
261
core/lib/Drupal/Core/Render/Element/Tableselect.php
Normal file
261
core/lib/Drupal/Core/Render/Element/Tableselect.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
61
core/lib/Drupal/Core/Render/Element/Tel.php
Normal file
61
core/lib/Drupal/Core/Render/Element/Tel.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
41
core/lib/Drupal/Core/Render/Element/Textarea.php
Normal file
41
core/lib/Drupal/Core/Render/Element/Textarea.php
Normal 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'),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
75
core/lib/Drupal/Core/Render/Element/Textfield.php
Normal file
75
core/lib/Drupal/Core/Render/Element/Textfield.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
47
core/lib/Drupal/Core/Render/Element/Token.php
Normal file
47
core/lib/Drupal/Core/Render/Element/Token.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
80
core/lib/Drupal/Core/Render/Element/Url.php
Normal file
80
core/lib/Drupal/Core/Render/Element/Url.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
30
core/lib/Drupal/Core/Render/Element/Value.php
Normal file
30
core/lib/Drupal/Core/Render/Element/Value.php
Normal 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,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
110
core/lib/Drupal/Core/Render/Element/VerticalTabs.php
Normal file
110
core/lib/Drupal/Core/Render/Element/VerticalTabs.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
81
core/lib/Drupal/Core/Render/Element/Weight.php
Normal file
81
core/lib/Drupal/Core/Render/Element/Weight.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
173
core/lib/Drupal/Core/Render/ElementInfoManager.php
Normal file
173
core/lib/Drupal/Core/Render/ElementInfoManager.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
71
core/lib/Drupal/Core/Render/ElementInfoManagerInterface.php
Normal file
71
core/lib/Drupal/Core/Render/ElementInfoManagerInterface.php
Normal 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);
|
||||
|
||||
}
|
46
core/lib/Drupal/Core/Render/HtmlResponse.php
Normal file
46
core/lib/Drupal/Core/Render/HtmlResponse.php
Normal 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);
|
||||
}
|
||||
}
|
218
core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php
Normal file
218
core/lib/Drupal/Core/Render/HtmlResponseAttachmentsProcessor.php
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
91
core/lib/Drupal/Core/Render/MainContent/AjaxRenderer.php
Normal file
91
core/lib/Drupal/Core/Render/MainContent/AjaxRenderer.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
97
core/lib/Drupal/Core/Render/MainContent/DialogRenderer.php
Normal file
97
core/lib/Drupal/Core/Render/MainContent/DialogRenderer.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
297
core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
Normal file
297
core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
46
core/lib/Drupal/Core/Render/MainContent/ModalRenderer.php
Normal file
46
core/lib/Drupal/Core/Render/MainContent/ModalRenderer.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
333
core/lib/Drupal/Core/Render/RenderCache.php
Normal file
333
core/lib/Drupal/Core/Render/RenderCache.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
77
core/lib/Drupal/Core/Render/RenderCacheInterface.php
Normal file
77
core/lib/Drupal/Core/Render/RenderCacheInterface.php
Normal 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);
|
||||
|
||||
}
|
30
core/lib/Drupal/Core/Render/RenderEvents.php
Normal file
30
core/lib/Drupal/Core/Render/RenderEvents.php
Normal 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';
|
||||
|
||||
}
|
641
core/lib/Drupal/Core/Render/Renderer.php
Normal file
641
core/lib/Drupal/Core/Render/Renderer.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
352
core/lib/Drupal/Core/Render/RendererInterface.php
Normal file
352
core/lib/Drupal/Core/Render/RendererInterface.php
Normal 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);
|
||||
|
||||
}
|
1216
core/lib/Drupal/Core/Render/theme.api.php
Normal file
1216
core/lib/Drupal/Core/Render/theme.api.php
Normal file
File diff suppressed because it is too large
Load diff
Reference in a new issue